"""Construye el workflow ComfyUI de UN sprite de personaje (identidad + pose + alpha).

Receta de personaje del report 0137, construible HOY en SD1.5 (todo verificado en
/object_info): IPAdapter-FaceID (identidad de rostro) + LoRA opcional (estilo/char)
+ ControlNet OpenPose (pose) + Rembg (transparencia). Produce el workflow de UNA
pose/vista; un sprite sheet completo se obtiene generando varias poses (cambiando
`pose_skeleton` + prompt, misma `seed` para identidad estable) y montandolas con
`comfyui_build_grid` / `comfyui_assemble_sprite_sheet` en un pipeline.

Compone funciones existentes del registry:
  - comfyui_build_ipadapter_workflow (mode='faceid')  -> base + identidad de cara
    (o comfyui_build_txt2img_workflow si no hay ref_image)
  - comfyui_inject_lora                                -> LoRA de personaje/estilo
  - ControlNetApplyAdvanced (helper local)             -> pose OpenPose con end<1.0
  - Image Rembg (Remove Background)  (helper local)    -> PNG con alpha

class_types/inputs verificados contra /object_info (8GB lowvram):
IPAdapterUnifiedLoaderFaceID/IPAdapterFaceID (FaceID solo SD1.5 instalado),
control_v11p_sd15_openpose_fp16.safetensors, ControlNetApplyAdvanced
(positive,negative,control_net,image,strength,start_percent,end_percent ->
positive,negative), 'Image Rembg (Remove Background)' (transparency BOOLEAN).

Por que ControlNetApplyAdvanced y no el legacy ControlNetApply (comfyui_inject_controlnet):
end_percent<1.0 baja la fuerza del esqueleto en los ultimos pasos para que el pelo
y la ropa no queden aplastados contra el OpenPose (report 0137).

Funcion pura: sin red, sin I/O. No muta dicts (copia profunda en los helpers).
"""
from __future__ import annotations

import copy
import os
import sys

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


def _is_link(v) -> bool:
    return isinstance(v, list) and len(v) == 2 and isinstance(v[0], str) and isinstance(v[1], int)


def _next_id(wf: dict) -> int:
    numeric = [int(k) for k in wf.keys() if str(k).isdigit()]
    return (max(numeric) + 1) if numeric else len(wf) + 1


def _inject_openpose_advanced(
    workflow: dict,
    skeleton_image: str,
    cn_name: str,
    strength: float,
    start_percent: float,
    end_percent: float,
) -> dict:
    """Inserta LoadImage + ControlNetLoader + ControlNetApplyAdvanced (positive+negative).

    Repunta KSampler.positive y KSampler.negative a las salidas del
    ControlNetApplyAdvanced. Mas fino que el legacy ControlNetApply: permite
    start/end percent (bajar la pose en los ultimos pasos).
    """
    wf = copy.deepcopy(workflow)
    ks_id = next(
        (nid for nid, n in wf.items() if str(n.get("class_type", "")).endswith("KSampler")), None
    )
    if ks_id is None:
        raise ValueError("comfyui_build_sprite_sheet_workflow: no se encontro KSampler")
    ks = wf[ks_id]["inputs"]
    if not (_is_link(ks.get("positive")) and _is_link(ks.get("negative"))):
        raise ValueError(
            "comfyui_build_sprite_sheet_workflow: el KSampler necesita positive y negative validos"
        )
    pos_src, neg_src = list(ks["positive"]), list(ks["negative"])

    base = _next_id(wf)
    load_id, loader_id, apply_id = str(base), str(base + 1), str(base + 2)
    wf[load_id] = {"class_type": "LoadImage", "inputs": {"image": skeleton_image}}
    wf[loader_id] = {"class_type": "ControlNetLoader", "inputs": {"control_net_name": cn_name}}
    wf[apply_id] = {
        "class_type": "ControlNetApplyAdvanced",
        "inputs": {
            "positive": pos_src,
            "negative": neg_src,
            "control_net": [loader_id, 0],
            "image": [load_id, 0],
            "strength": strength,
            "start_percent": start_percent,
            "end_percent": end_percent,
        },
    }
    wf[ks_id]["inputs"]["positive"] = [apply_id, 0]
    wf[ks_id]["inputs"]["negative"] = [apply_id, 1]
    return wf


def _inject_rembg(workflow: dict, model: str) -> dict:
    """Inserta 'Image Rembg (Remove Background)' (transparency=True) entre VAEDecode y SaveImage."""
    wf = copy.deepcopy(workflow)
    vaedecode_id = next(
        (nid for nid, n in wf.items() if n.get("class_type") == "VAEDecode"), None
    )
    save_id = next((nid for nid, n in wf.items() if n.get("class_type") == "SaveImage"), None)
    if vaedecode_id is None or save_id is None:
        raise ValueError(
            "comfyui_build_sprite_sheet_workflow: no se encontro VAEDecode/SaveImage para Rembg"
        )
    rembg_id = str(_next_id(wf))
    wf[rembg_id] = {
        "class_type": "Image Rembg (Remove Background)",
        "inputs": {
            "images": [vaedecode_id, 0],
            "transparency": True,
            "model": 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",
        },
    }
    wf[save_id]["inputs"]["images"] = [rembg_id, 0]
    return wf


def comfyui_build_sprite_sheet_workflow(
    subject: str,
    *,
    ref_image: str | None = None,
    pose_skeleton: str | None = None,
    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.55,
    controlnet_start: float = 0.0,
    controlnet_end: float = 0.8,
    transparent: bool = True,
    rembg_model: str = "u2net",
    weight: float = 0.75,
    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",
    filename_prefix: str = "sprite",
) -> dict:
    """Construye el dict (API format) del workflow de un sprite de personaje.

    Args:
        subject: descripcion del personaje (ej. "a knight in silver armor"). Se
            completa con ", full body, game sprite, simple background". No vacio.
        ref_image: imagen de referencia de rostro en input/ del servidor. Si se
            pasa, usa IPAdapter-FaceID (identidad consistente). None = identidad
            solo por prompt + seed. keyword-only.
        pose_skeleton: imagen de esqueleto OpenPose en input/ para fijar la pose
            via ControlNet. None = pose libre. keyword-only.
        ckpt_name: checkpoint SD1.5 (FaceID + OpenPose solo instalados en SD1.5;
            default 'IMG_dreamshaper_8.safetensors'). keyword-only.
        char_lora: LoRA de personaje/estilo opcional en models/loras.
        lora_strength: fuerza del char_lora sobre model y clip.
        controlnet_name: ControlNet OpenPose (default SD1.5).
        controlnet_strength: fuerza del OpenPose (recomendado 0.55).
        controlnet_start, controlnet_end: rango de aplicacion del OpenPose
            (end<1.0 deja libres los ultimos pasos para pelo/ropa).
        transparent: si True inyecta Rembg para PNG con alpha. False = opaco.
        rembg_model: modelo Rembg ('u2net' general, 'isnet-anime' para anime).
        weight: peso del IPAdapter-FaceID (solo si ref_image).
        negative, width, height, steps, cfg, seed, sampler_name, scheduler,
            filename_prefix: parametros de generacion. width 512 x height 768
            (vertical, encuadra cuerpo entero).

    Returns:
        dict en API format listo para comfyui_submit_workflow: base txt2img (o
        IPAdapter-FaceID) + LoRA opcional + ControlNet OpenPose (si pose_skeleton)
        + Rembg (si transparent). Es UN sprite; varias poses -> sprite sheet.

    Raises:
        ValueError: si subject esta vacio, o si la base no tiene KSampler/
            VAEDecode/SaveImage donde inyectar (propagado por los helpers).
    """
    from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow

    if not subject or not subject.strip():
        raise ValueError("comfyui_build_sprite_sheet_workflow: 'subject' no puede estar vacio")

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

    if ref_image:
        from ml.comfyui_build_ipadapter_workflow import comfyui_build_ipadapter_workflow

        wf = comfyui_build_ipadapter_workflow(
            positive,
            ref_image,
            base_checkpoint=ckpt_name,
            mode="faceid",
            weight=weight,
            negative=negative,
            steps=steps,
            cfg=cfg,
            width=width,
            height=height,
            seed=seed,
            sampler_name=sampler_name,
            scheduler=scheduler,
            filename_prefix=filename_prefix,
        )
    else:
        wf = comfyui_build_txt2img_workflow(
            ckpt_name,
            positive,
            negative,
            steps=steps,
            cfg=cfg,
            width=width,
            height=height,
            seed=seed,
            sampler_name=sampler_name,
            scheduler=scheduler,
            filename_prefix=filename_prefix,
        )

    if char_lora:
        from ml.comfyui_inject_lora import comfyui_inject_lora

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

    if pose_skeleton:
        wf = _inject_openpose_advanced(
            wf,
            pose_skeleton,
            controlnet_name,
            controlnet_strength,
            controlnet_start,
            controlnet_end,
        )

    if transparent:
        wf = _inject_rembg(wf, rembg_model)

    return wf


if __name__ == "__main__":
    import json

    wf = comfyui_build_sprite_sheet_workflow(
        "a knight in silver armor",
        ref_image="faceref.png",
        pose_skeleton="pose_front.png",
        transparent=True,
        seed=5,
    )
    print(json.dumps({"nodes": list(wf), "classes": sorted({n["class_type"] for n in wf.values()})}, indent=2))
