"""Construye el workflow ComfyUI que PINTA un sprite a partir del BOCETO del dev (ControlNet).

Eje distinto al de los builders gamedev hermanos:

  - txt2img (enemy_creature, item_icon, ui_hud...) -> parte de TEXTO en blanco: la IA
    inventa la forma desde ruido. No hay dibujo de partida.
  - img2img (asset_variant) -> parte de una imagen YA PINTADA y la reescribe (material,
    paleta, tier) conservando la composicion via denoise medio.
  - sketch -> ControlNet (ESTE builder) -> parte del DIBUJO TOSCO del dev (un boceto,
    un lineart a lapiz, un garabato) y lo PINTA en estilo de juego RESPETANDO las lineas
    dibujadas. El dev marca la forma; la IA pone el material, el color y el acabado.

Es txt2img guiado por ControlNet: el KSampler arranca de ruido (EmptyLatentImage, denoise
1.0, como txt2img) pero su condicionamiento positivo pasa por un ControlNetApply que lo
ata al mapa de lineas extraido del boceto. El resultado tiene la SILUETA y la estructura
del dibujo del dev, pintadas con el prompt de `subject` + `style`.

Cableado:

    CheckpointLoaderSimple -> [LoraLoader opcional] -> KSampler.model
    EmptyLatentImage ----------------------------------> KSampler.latent  (ruido, txt2img)
    CLIPTextEncode(positive) -> ControlNetApply.conditioning
    LoadImage(boceto) -> [Preprocessor lineart/scribble/canny] -> ControlNetApply.image
    ControlNetLoader(modelo CN)                          -> ControlNetApply.control_net
    ControlNetApply -> KSampler.positive
    KSampler -> VAEDecode -> SaveImage

Compone (registry-first, sin reescribir nada):
  - comfyui_build_txt2img_workflow -> base txt2img (ruido + KSampler denoise 1.0)
  - comfyui_inject_controlnet      -> rama LoadImage + ControlNetLoader + ControlNetApply
                                      repuntando KSampler.positive
  - comfyui_inject_lora            -> LoRA de estilo opcional (consistencia con el set)

El unico codigo propio es `_inject_preprocessor`: inserta el nodo preprocesador
(LineArtPreprocessor / ScribblePreprocessor / CannyEdgePreprocessor) entre el LoadImage
del boceto y el ControlNetApply.image, de modo que el ControlNet reciba un MAPA DE LINEAS
limpio en lugar del boceto crudo. Es el analogo a `_inject_image_scale` del builder
hermano comfyui_build_asset_variant_workflow (mismo patron de helper privado especifico
del builder).

`control_type` selecciona el preprocesador y, por defecto, el modelo ControlNet emparejado:

    control_type   preprocesador (extrae lineas)   modelo CN ideal
    -----------    -----------------------------   ---------------------------------------
    lineart        LineArtPreprocessor             control_v11p_sd15_lineart_fp16.safetensors
    scribble       ScribblePreprocessor            control_v11p_sd15_scribble_fp16.safetensors
    canny          CannyEdgePreprocessor           control_v11p_sd15_canny_fp16.safetensors

`canny` se elige cuando el boceto tiene bordes nitidos; `lineart` para dibujos a lapiz/tinta;
`scribble` para garabatos sueltos donde se quiere que la IA tenga mas libertad de interpretacion.

GOTCHA del servidor 8GB lowvram actual: SOLO estan instalados los modelos ControlNet SD1.5
canny / depth / openpose. Los modelos dedicados control_v11p_sd15_lineart_fp16 y
control_v11p_sd15_scribble_fp16 NO estan presentes (pendiente humano descargarlos). Mientras
tanto: usa `control_type="canny"` (preprocesador + modelo, todo instalado), o pasa
`controlnet_name="control_v11p_sd15_canny_fp16.safetensors"` como override para lineart/scribble
(el modelo canny tolera mapas de lineas de cualquier preprocesador, con algo menos de fidelidad
que el modelo dedicado). Los PREPROCESADORES (LineArt/Scribble/Canny) SI estan todos instalados
(custom node comfyui_controlnet_aux).

class_types/inputs verificados contra /object_info del servidor (8GB lowvram):
CheckpointLoaderSimple, EmptyLatentImage, CLIPTextEncode, KSampler, VAEDecode, SaveImage,
LoadImage, ControlNetLoader, ControlNetApply, LineArtPreprocessor, ScribblePreprocessor,
CannyEdgePreprocessor, LoraLoader.

Funcion pura: sin red, sin I/O. No muta dicts de entrada (copia profunda al insertar el
preprocesador). NO valida que sketch_image/checkpoint/controlnet_name/lora existan en el
servidor (eso es responsabilidad de comfyui_validate_workflow antes de enviar). Determinista
para los mismos argumentos.
"""
from __future__ import annotations

import copy
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

# Mapa control_type -> (clase del preprocesador, modelo ControlNet ideal emparejado).
# El preprocesador extrae el mapa de lineas del boceto; el modelo lo aplica al
# condicionamiento. Ver el GOTCHA del modulo: en el server 8GB solo canny tiene
# ambos lados instalados; lineart/scribble requieren override del modelo o descarga.
_CONTROL_MAP = {
    "lineart": ("LineArtPreprocessor", "control_v11p_sd15_lineart_fp16.safetensors"),
    "scribble": ("ScribblePreprocessor", "control_v11p_sd15_scribble_fp16.safetensors"),
    "canny": ("CannyEdgePreprocessor", "control_v11p_sd15_canny_fp16.safetensors"),
}

# Negativo por defecto para un sprite pintado desde boceto: UNA figura entera, bien
# formada, fondo limpio, sin texto/marcas ni objetos extra. El ControlNet ya impone la
# forma; el negativo solo limpia ruido tipico de SD1.5.
_SKETCH_NEGATIVE = (
    "blurry, lowres, deformed, disfigured, bad anatomy, extra limbs, "
    "duplicate, multiple subjects, text, watermark, signature, logo, "
    "cluttered background, cropped, cut off, out of frame, jpeg artifacts"
)


def _inject_preprocessor(workflow: dict, *, control_type: str, resolution: int) -> dict:
    """Inserta un nodo preprocesador entre el LoadImage del boceto y el ControlNetApply.

    Tras comfyui_inject_controlnet, el ControlNetApply recibe la imagen directamente del
    LoadImage del boceto. Este helper interpone el preprocesador correspondiente al
    control_type (LineArt/Scribble/Canny), que convierte el boceto crudo en un mapa de
    lineas limpio, y repunta ControlNetApply.image a la salida del preprocesador.

    Pura: trabaja sobre una copia profunda. Determinista.

    Raises:
        ValueError: si no se encuentra ControlNetApply, o si el ControlNetApply no recibe
            su imagen de un LoadImage (cableado inesperado).
    """
    wf = copy.deepcopy(workflow)

    apply_id = next(
        (nid for nid, n in wf.items() if n.get("class_type") == "ControlNetApply"), None
    )
    if apply_id is None:
        raise ValueError(
            "comfyui_build_sprite_from_sketch_workflow: no se encontro ControlNetApply "
            "para insertar el preprocesador"
        )

    # Fuente de la imagen de control que hoy alimenta el ControlNetApply (el LoadImage).
    src = wf[apply_id]["inputs"].get("image")
    if not (isinstance(src, list) and len(src) == 2):
        raise ValueError(
            "comfyui_build_sprite_from_sketch_workflow: ControlNetApply.image no apunta a "
            "un nodo (cableado inesperado del boceto)"
        )

    preproc_class = _CONTROL_MAP[control_type][0]
    inputs: dict = {"image": list(src), "resolution": int(resolution)}
    if control_type == "lineart":
        inputs["coarse"] = "disable"
    elif control_type == "canny":
        inputs["low_threshold"] = 100
        inputs["high_threshold"] = 200
    # scribble no tiene parametros extra mas alla de resolution.

    numeric = [int(k) for k in wf.keys() if str(k).isdigit()]
    preproc_id = str((max(numeric) + 1) if numeric else len(wf) + 1)
    wf[preproc_id] = {"class_type": preproc_class, "inputs": inputs}

    # Repunta el ControlNetApply para que reciba el MAPA DE LINEAS preprocesado.
    wf[apply_id]["inputs"]["image"] = [preproc_id, 0]
    return wf


def comfyui_build_sprite_from_sketch_workflow(
    sketch_image: str,
    subject: str,
    *,
    control_type: str = "lineart",
    checkpoint: str = "IMG_dreamshaper_8.safetensors",
    style: str = "game asset, clean, centered",
    strength: float = 0.8,
    size: int = 512,
    seed: int = 0,
    lora: str | None = None,
    lora_strength: float = 1.0,
    preprocess: bool = True,
    controlnet_name: str | None = None,
    negative: str | None = None,
    steps: int = 28,
    cfg: float = 7.0,
    sampler_name: str = "dpmpp_2m",
    scheduler: str = "karras",
    filename_prefix: str = "sprite_from_sketch",
) -> dict:
    """Construye el dict (API format) del workflow boceto->sprite guiado por ControlNet.

    A partir del DIBUJO TOSCO del dev (boceto, lineart, garabato) + un prompt de QUE es,
    genera un sprite pintado en estilo de juego que CONSERVA la forma dibujada. Es txt2img
    (arranca de ruido) condicionado por un ControlNet atado al mapa de lineas del boceto.

    Args:
        sketch_image: nombre del archivo del boceto dentro de la carpeta input/ del servidor
            ComfyUI (lo carga el nodo LoadImage). Subelo antes con POST /upload/image o
            copialo a ~/ComfyUI/input/. Puede ser un boceto a lapiz, un lineart limpio o un
            garabato; el preprocesador lo normaliza a un mapa de lineas. No puede estar vacio.
        subject: descripcion de QUE representa el boceto, para que la IA sepa que pintar
            sobre la forma (ej. "armored knight", "treasure chest", "fire goblin"). Es el
            prompt positivo: NO inventa la silueta (eso lo fija el ControlNet), pone material,
            color y acabado. No puede estar vacio.
        control_type: preprocesador y modelo ControlNet a usar. keyword-only.
            - "lineart" (default): LineArtPreprocessor. Para dibujos a lapiz/tinta.
            - "scribble": ScribblePreprocessor. Para garabatos sueltos; mas libertad a la IA.
            - "canny": CannyEdgePreprocessor. Para bocetos con bordes nitidos. Es el unico
              cuyo modelo CN dedicado esta instalado en el server 8GB actual (ver GOTCHA del
              modulo); funciona out-of-the-box.
        checkpoint: checkpoint del servidor. 'IMG_dreamshaper_8.safetensors' (SD1.5, holgado en
            8GB lowvram) por defecto. keyword-only.
        style: descriptor de estilo que mantiene coherentes los sprites de un set (ej.
            "game asset, clean, centered", "dark fantasy creature", "pixel art"). Mismo style
            + checkpoint + (lora) para todos los sprites del mismo juego. keyword-only.
        strength: fuerza del ControlNet (cuanto respeta el boceto). 0.0 = ignora el dibujo
            (txt2img puro); 1.0 = se ciñe estrictamente a las lineas. 0.8 (recomendado) deja
            algo de margen a la IA para interpretar. Se clampa a [0.0, 2.0]. keyword-only.
        size: lado en px del sprite generado (EmptyLatentImage size x size). El preprocesador
            normaliza el boceto a esta resolucion. 512 por defecto (SD1.5). keyword-only.
        seed: semilla del KSampler. keyword-only.
        lora: LoRA de estilo opcional en models/loras (ej. 'SD15_dark_fantasy.safetensors').
            None = sin LoRA. keyword-only.
        lora_strength: fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0]. keyword-only.
        preprocess: si True (default), inserta el preprocesador del control_type entre el
            boceto y el ControlNet (recomendado: el boceto crudo casi nunca es un mapa de
            control limpio). Si False, el boceto se pasa DIRECTO al ControlNet como mapa de
            lineas (util solo cuando sketch_image YA es un lineart blanco-sobre-negro limpio).
            keyword-only.
        controlnet_name: override explicito del modelo ControlNet en models/controlnet. None
            = usa el modelo emparejado al control_type segun _CONTROL_MAP. Pasa aqui
            'control_v11p_sd15_canny_fp16.safetensors' para degradar lineart/scribble al
            modelo canny disponible (ver GOTCHA del modulo). keyword-only.
        negative: prompt negativo. None usa el negativo por defecto pensado para sprites
            (una figura, fondo limpio, sin texto). keyword-only.
        steps, cfg, sampler_name, scheduler, filename_prefix: parametros de generacion.
            keyword-only.

    Returns:
        dict en API format listo para comfyui_submit_workflow: txt2img base + rama ControlNet
        (LoadImage del boceto -> [Preprocessor si preprocess] -> ControlNetApply -> KSampler.
        positive) + LoRA de estilo opcional. Es UN sprite; varios objetos del mismo set ->
        llamar por `subject`/`sketch_image` con el mismo style/checkpoint/(lora).

    Raises:
        ValueError: si sketch_image o subject estan vacios, o si control_type no es uno de
            'lineart' / 'scribble' / 'canny'.
    """
    from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
    from ml.comfyui_inject_controlnet import comfyui_inject_controlnet

    if not sketch_image or not sketch_image.strip():
        raise ValueError(
            "comfyui_build_sprite_from_sketch_workflow: 'sketch_image' no puede estar vacio "
            "(el ControlNet necesita el boceto en input/)."
        )
    if not subject or not subject.strip():
        raise ValueError(
            "comfyui_build_sprite_from_sketch_workflow: 'subject' no puede estar vacio "
            "(la IA necesita saber QUE pintar sobre la forma del boceto)."
        )
    if control_type not in _CONTROL_MAP:
        raise ValueError(
            "comfyui_build_sprite_from_sketch_workflow: control_type "
            f"'{control_type}' invalido; usa uno de {sorted(_CONTROL_MAP)}."
        )

    sketch_image = sketch_image.strip()
    subject = subject.strip()
    strength = max(0.0, min(2.0, float(strength)))
    lora_strength = max(0.0, min(2.0, float(lora_strength)))
    neg = _SKETCH_NEGATIVE if negative is None else negative
    cn_model = controlnet_name or _CONTROL_MAP[control_type][1]

    # Prompt positivo: describe el sujeto y el estilo; la SILUETA la impone el ControlNet,
    # asi que aqui solo se empuja material/acabado y "single subject, full body".
    positive = (
        f"{subject}, {style}, single subject, full body, "
        "painted, polished, high detail, consistent design"
    )

    wf = comfyui_build_txt2img_workflow(
        checkpoint,
        positive,
        neg,
        steps=steps,
        cfg=cfg,
        width=size,
        height=size,
        seed=seed,
        sampler_name=sampler_name,
        scheduler=scheduler,
        filename_prefix=filename_prefix,
    )

    # Rama ControlNet: LoadImage(boceto) + ControlNetLoader + ControlNetApply, repuntando
    # KSampler.positive. Reutiliza el inyector encadenable del registry.
    wf = comfyui_inject_controlnet(wf, sketch_image, cn_model, strength=strength)

    # Interpone el preprocesador del control_type entre el boceto y el ControlNet.
    if preprocess:
        wf = _inject_preprocessor(wf, control_type=control_type, resolution=size)

    if lora:
        from ml.comfyui_inject_lora import comfyui_inject_lora

        wf = comfyui_inject_lora(
            wf, lora, strength_model=lora_strength, strength_clip=lora_strength
        )

    return wf


if __name__ == "__main__":
    import json

    wf = comfyui_build_sprite_from_sketch_workflow(
        "knight_sketch.png",
        "armored knight, fantasy hero",
        control_type="lineart",
        style="game asset, clean, centered",
        strength=0.8,
        seed=7,
    )
    print(
        json.dumps(
            {
                "nodes": list(wf),
                "classes": sorted({n["class_type"] for n in wf.values()}),
                "control_type": "lineart",
                "cn_model": next(
                    n["inputs"]["control_net_name"]
                    for n in wf.values()
                    if n["class_type"] == "ControlNetLoader"
                ),
                "preprocessor": next(
                    n["class_type"]
                    for n in wf.values()
                    if n["class_type"].endswith("Preprocessor")
                ),
                "strength": next(
                    n["inputs"]["strength"]
                    for n in wf.values()
                    if n["class_type"] == "ControlNetApply"
                ),
                "positive": wf["6"]["inputs"]["text"],
            },
            indent=2,
        )
    )
