"""Construye el workflow ComfyUI de un WALK CYCLE animado (N frames pose-driven -> WEBP).

Caso 1 del report 0217 ("animacion de sprite frame-by-frame pose-driven"): a partir de
N esqueletos OpenPose que describen las poses sucesivas de un ciclo de andar, construye el
dict (API format) de un workflow que:

  1. genera un frame por pose con la MISMA seed y el MISMO prompt/checkpoint/LoRA, de modo
     que el personaje conserva su identidad de un frame al siguiente (la unica variable es
     el esqueleto OpenPose que dicta la postura);
  2. combina los N frames en un unico batch encadenando `ImageBatch`;
  3. recorta el fondo a alpha con `Image Rembg (Remove Background)` (transparencia para el
     motor del juego);
  4. los exporta como WEBP animado con `SaveAnimatedWEBP` (un solo archivo reproducible).

Es el builder PURO equivalente a `comfyui_build_sprite_sheet_workflow` (que produce UN
frame estatico) pero orientado a ANIMACION: en vez de devolver un sprite suelto por pose y
montar un contact-sheet a posteriori, este grafo produce de una sola pasada el WEBP animado
del ciclo. Hermano direccional: `comfyui_build_directional_sprite_workflow` (rota el
personaje en 3D); aqui el personaje no rota, camina (mismo angulo de camara, poses 2D).

Por que ControlNetApplyAdvanced (y no el legacy ControlNetApply): `end_percent` < 1.0 deja
los ultimos pasos del sampler libres para que pelo y ropa no queden aplastados contra el
esqueleto OpenPose (mismo razonamiento que el sprite sheet, report 0137).

Por que la seed es FIJA para todos los frames: una seed distinta por frame haria que el
personaje "parpadee" de identidad entre fotogramas (ropa/cara/paleta derivan). Compartir la
seed + prompt + checkpoint y variar solo el OpenPose es lo que hace que sea el mismo
personaje andando, no N personajes distintos en N posturas.

Funcion PURA: sin red, sin I/O. No muta las entradas (no recibe dicts; copia la lista de
poses). Todos los class_types y sus inputs estan verificados contra /object_info del server
8GB (CheckpointLoaderSimple, LoraLoader, CLIPTextEncode, ControlNetLoader, LoadImage,
ControlNetApplyAdvanced, EmptyLatentImage, KSampler, VAEDecode, ImageBatch,
'Image Rembg (Remove Background)', SaveAnimatedWEBP).
"""
from __future__ import annotations


def comfyui_build_walk_cycle_workflow(
    subject: str,
    pose_skeletons: list,
    *,
    ckpt_name: str = "IMG_dreamshaper_8.safetensors",
    char_lora: str | None = None,
    lora_strength: float = 1.0,
    controlnet_name: str = "control_v11p_sd15_openpose_fp16.safetensors",
    controlnet_strength: float = 0.7,
    controlnet_start: float = 0.0,
    controlnet_end: float = 0.8,
    transparent: bool = True,
    rembg_model: str = "u2net",
    negative: str = "blurry, lowres, extra limbs, deformed",
    width: int = 512,
    height: int = 768,
    steps: int = 24,
    cfg: float = 7.0,
    seed: int = 0,
    sampler_name: str = "dpmpp_2m",
    scheduler: str = "karras",
    fps: int = 8,
    filename_prefix: str = "walk_cycle",
) -> dict:
    """Construye el dict (API format) del workflow de un walk cycle animado.

    Genera un frame por cada esqueleto OpenPose de ``pose_skeletons`` con identidad
    consistente (misma seed/prompt/checkpoint), los combina en un batch, los recorta a
    alpha (Rembg) y los guarda como WEBP animado.

    Args:
        subject: descripcion del personaje (ej. "pixel art knight"). Se completa con
            ", full body, game sprite, simple background, walking". No puede estar vacio.
        pose_skeletons: lista (no vacia) de nombres de archivo de esqueletos OpenPose en el
            dir ``input/`` del servidor (uno por frame del ciclo, en orden de animacion). El
            grafo crea un LoadImage por entrada; cada uno debe ser un string no vacio. La
            lista no se muta.
        ckpt_name: checkpoint SD1.5 (OpenPose solo instalado en SD1.5; default
            'IMG_dreamshaper_8.safetensors'). keyword-only.
        char_lora: LoRA de personaje/estilo opcional en models/loras (refuerza la
            consistencia de ropa/cuerpo entre frames). None = sin LoRA. keyword-only.
        lora_strength: fuerza del char_lora sobre model y clip. keyword-only.
        controlnet_name: ControlNet OpenPose (default SD1.5). keyword-only.
        controlnet_strength: fuerza del OpenPose (default 0.7). keyword-only.
        controlnet_start: inicio de aplicacion del OpenPose (fraccion 0..1). keyword-only.
        controlnet_end: fin de aplicacion del OpenPose (end<1.0 deja libres los ultimos
            pasos para pelo/ropa; default 0.8). keyword-only.
        transparent: si True inyecta Rembg para alpha (recomendado para sprites de juego).
            False = fondo opaco. keyword-only.
        rembg_model: modelo Rembg ('u2net' general, 'isnet-anime' para anime). keyword-only.
        negative: prompt negativo. keyword-only.
        width: ancho en px (512). keyword-only.
        height: alto en px (768, vertical, encuadra cuerpo entero). keyword-only.
        steps: pasos del KSampler. keyword-only.
        cfg: CFG del KSampler. keyword-only.
        seed: semilla del KSampler, FIJA e identica para todos los frames (identidad
            consistente). keyword-only.
        sampler_name: sampler del KSampler (default 'dpmpp_2m'). keyword-only.
        scheduler: scheduler del KSampler (default 'karras'). keyword-only.
        fps: frames por segundo del WEBP animado (default 8). keyword-only.
        filename_prefix: prefijo del archivo WEBP en output/ (default 'walk_cycle').
            keyword-only.

    Returns:
        dict en API format listo para comfyui_submit_workflow. Las claves son node_ids
        (string) y cada valor tiene class_type + inputs. Estructura: CheckpointLoaderSimple
        (+ LoraLoader si char_lora) + 2x CLIPTextEncode + ControlNetLoader compartido +
        N x (LoadImage + ControlNetApplyAdvanced + EmptyLatentImage + KSampler + VAEDecode)
        + cadena de ImageBatch que une los N frames + Rembg (si transparent) +
        SaveAnimatedWEBP.

    Raises:
        ValueError: si subject esta vacio, pose_skeletons esta vacio, o alguna pose no es un
            string no vacio.
    """
    if not subject or not subject.strip():
        raise ValueError("comfyui_build_walk_cycle_workflow: 'subject' no puede estar vacio")
    if not isinstance(pose_skeletons, (list, tuple)) or len(pose_skeletons) == 0:
        raise ValueError(
            "comfyui_build_walk_cycle_workflow: 'pose_skeletons' debe ser una lista no vacia "
            "de nombres de esqueletos OpenPose en input/ (uno por frame del ciclo)."
        )
    poses = list(pose_skeletons)
    for i, p in enumerate(poses):
        if not isinstance(p, str) or not p.strip():
            raise ValueError(
                "comfyui_build_walk_cycle_workflow: pose_skeletons["
                f"{i}] debe ser un string no vacio (nombre de archivo en input/)."
            )

    positive = f"{subject}, full body, game sprite, simple background, walking"

    wf: dict = {}
    counter = [0]

    def nid() -> str:
        counter[0] += 1
        return str(counter[0])

    # 1. Checkpoint -> MODEL(0), CLIP(1), VAE(2).
    ckpt_id = nid()
    wf[ckpt_id] = {
        "class_type": "CheckpointLoaderSimple",
        "inputs": {"ckpt_name": ckpt_name},
    }
    model_src = [ckpt_id, 0]
    clip_src = [ckpt_id, 1]
    vae_src = [ckpt_id, 2]

    # 2. LoRA opcional -> reapunta MODEL/CLIP a su salida.
    if char_lora:
        lora_id = nid()
        wf[lora_id] = {
            "class_type": "LoraLoader",
            "inputs": {
                "model": model_src,
                "clip": clip_src,
                "lora_name": char_lora,
                "strength_model": lora_strength,
                "strength_clip": lora_strength,
            },
        }
        model_src = [lora_id, 0]
        clip_src = [lora_id, 1]

    # 3. Prompts positivo y negativo (compartidos por todos los frames).
    pos_clip_id = nid()
    wf[pos_clip_id] = {
        "class_type": "CLIPTextEncode",
        "inputs": {"text": positive, "clip": clip_src},
    }
    neg_clip_id = nid()
    wf[neg_clip_id] = {
        "class_type": "CLIPTextEncode",
        "inputs": {"text": negative, "clip": clip_src},
    }

    # 4. ControlNetLoader compartido (uno solo para todas las poses).
    cn_loader_id = nid()
    wf[cn_loader_id] = {
        "class_type": "ControlNetLoader",
        "inputs": {"control_net_name": controlnet_name},
    }

    # 5. Por cada pose: LoadImage -> ControlNetApplyAdvanced -> EmptyLatent -> KSampler -> VAEDecode.
    frame_image_srcs: list = []
    for pose in poses:
        load_id = nid()
        wf[load_id] = {"class_type": "LoadImage", "inputs": {"image": pose}}

        apply_id = nid()
        wf[apply_id] = {
            "class_type": "ControlNetApplyAdvanced",
            "inputs": {
                "positive": [pos_clip_id, 0],
                "negative": [neg_clip_id, 0],
                "control_net": [cn_loader_id, 0],
                "image": [load_id, 0],
                "strength": controlnet_strength,
                "start_percent": controlnet_start,
                "end_percent": controlnet_end,
            },
        }

        latent_id = nid()
        wf[latent_id] = {
            "class_type": "EmptyLatentImage",
            "inputs": {"width": width, "height": height, "batch_size": 1},
        }

        ksampler_id = nid()
        wf[ksampler_id] = {
            "class_type": "KSampler",
            "inputs": {
                "seed": seed,  # FIJA: misma seed para todos los frames (identidad consistente).
                "steps": steps,
                "cfg": cfg,
                "sampler_name": sampler_name,
                "scheduler": scheduler,
                "denoise": 1.0,
                "model": model_src,
                "positive": [apply_id, 0],
                "negative": [apply_id, 1],
                "latent_image": [latent_id, 0],
            },
        }

        vae_id = nid()
        wf[vae_id] = {
            "class_type": "VAEDecode",
            "inputs": {"samples": [ksampler_id, 0], "vae": vae_src},
        }
        frame_image_srcs.append([vae_id, 0])

    # 6. Combinar los N frames en un solo batch encadenando ImageBatch.
    if len(frame_image_srcs) == 1:
        batch_src = frame_image_srcs[0]
    else:
        batch_src = frame_image_srcs[0]
        for next_src in frame_image_srcs[1:]:
            ib_id = nid()
            wf[ib_id] = {
                "class_type": "ImageBatch",
                "inputs": {"image1": batch_src, "image2": next_src},
            }
            batch_src = [ib_id, 0]

    # 7. Rembg opcional sobre el batch (alpha para el motor del juego).
    save_images_src = batch_src
    if transparent:
        rembg_id = nid()
        wf[rembg_id] = {
            "class_type": "Image Rembg (Remove Background)",
            "inputs": {
                "images": batch_src,
                "transparency": True,
                "model": rembg_model,
                "post_processing": False,
                "only_mask": False,
                "alpha_matting": False,
                "alpha_matting_foreground_threshold": 240,
                "alpha_matting_background_threshold": 10,
                "alpha_matting_erode_size": 10,
                "background_color": "none",
            },
        }
        save_images_src = [rembg_id, 0]

    # 8. Exportar el ciclo como WEBP animado.
    save_id = nid()
    wf[save_id] = {
        "class_type": "SaveAnimatedWEBP",
        "inputs": {
            "images": save_images_src,
            "filename_prefix": filename_prefix,
            "fps": float(fps),
            "lossless": True,
            "quality": 90,
            "method": "default",
        },
    }

    return wf


if __name__ == "__main__":
    import json

    wf = comfyui_build_walk_cycle_workflow(
        "pixel art knight",
        pose_skeletons=[
            "walk_pose_00.png",
            "walk_pose_01.png",
            "walk_pose_02.png",
            "walk_pose_03.png",
        ],
    )
    print(json.dumps({
        "nodes": list(wf),
        "classes": sorted({n["class_type"] for n in wf.values()}),
    }, indent=2))
