"""Construye el workflow ComfyUI de UN retrato/avatar de personaje (API format).

Retratos/avatares de personaje para dialogo, pantalla de perfil o seleccion de
personaje: busto centrado (cabeza y hombros), cara mirando al espectador, fondo
simple con vinheta, estilo consistente entre retratos. Es el builder hermano de
comfyui_build_sprite_sheet_workflow / comfyui_build_item_icon_workflow: mismo
patron (PURO, dict API format) que compone funciones existentes del registry, no
reescribe el grafo.

Cableado segun los argumentos:

    [IPAdapter-FaceID si ref_face]                 -> rostro consistente
    CheckpointLoaderSimple -> [LoraLoader si lora] -> KSampler
                          -> CLIPTextEncode (scaffold de retrato) ...
                          -> VAEDecode -> SaveImage
                          -> [FaceDetailer si facedetailer]  -> SaveImage final

Compone:
  - comfyui_build_ipadapter_workflow (mode='faceid')  -> identidad de rostro
    consistente desde una imagen de referencia (o comfyui_build_txt2img_workflow
    si no hay ref_face)
  - comfyui_inject_lora                                -> LoRA de estilo opcional
  - comfyui_build_facedetailer_workflow (modo dict)    -> cara nitida (regenera la
    cara detectada con un sampler de difusion, el 'pain #1' de los retratos)

Por que IPAdapter-FaceID y no solo prompt: para que el MISMO personaje aparezca
en varios retratos (varias expresiones/poses) hace falta fijar el rostro; FaceID
extrae el embedding de la cara de `ref_face` y lo impone (report 0137). Sin
`ref_face`, la identidad la dan solo el prompt + la seed (estable entre llamadas
con la misma seed, pero no anclado a una cara concreta).

Por que FaceDetailer al final: el primer render pierde detalle en la cara a 512px
(ojos, piel); FaceDetailer detecta la cara (YOLO) y la regenera ampliada. Cuando
se activa, se descarta el SaveImage base para que el unico PNG guardado sea el de
la cara ya refinada.

class_types/inputs verificados contra /object_info del servidor (8GB lowvram) a
traves de los builders que compone (IPAdapterUnifiedLoaderFaceID/IPAdapterFaceID,
LoraLoader, UltralyticsDetectorProvider/FaceDetailer de Impact-Pack).

Funcion pura: sin red, sin I/O. No muta dicts de entrada (los builders/inyectores
que compone trabajan sobre copias). Determinista para los mismos argumentos.
"""
from __future__ import annotations

import os
import sys

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

# Negativo por defecto pensado para retratos: una sola cara bien formada, sin
# texto/marcas ni recortes que ensucien el busto centrado.
_PORTRAIT_NEGATIVE = (
    "blurry, lowres, deformed face, disfigured, bad anatomy, extra limbs, "
    "extra fingers, mutated hands, ugly, text, watermark, signature, "
    "cropped, out of frame, multiple faces, two heads, jpeg artifacts"
)


def comfyui_build_portrait_avatar_workflow(
    character: str,
    *,
    style: str = "character portrait",
    ref_face: str | None = None,
    checkpoint: str = "IMG_dreamshaper_8.safetensors",
    size: int = 512,
    facedetailer: bool = True,
    seed: int = 0,
    lora: str | None = None,
    lora_strength: float = 1.0,
    weight: float = 0.85,
    negative: str | None = None,
    steps: int = 28,
    cfg: float = 7.0,
    sampler_name: str = "dpmpp_2m",
    scheduler: str = "karras",
    fd_denoise: float = 0.45,
    bbox_model: str = "face_yolov8m.pt",
    filename_prefix: str = "portrait",
) -> dict:
    """Construye el dict (API format) del workflow de un retrato/avatar de personaje.

    Args:
        character: descripcion del personaje (ej. "a young knight woman with red
            hair", "an old wizard with a long white beard"). Se inserta en un
            prompt scaffold de retrato. No puede estar vacio.
        style: descriptor de estilo que mantiene consistentes los retratos de un
            set (ej. "character portrait", "anime portrait", "realistic RPG
            portrait", "oil painting portrait"). Pasa el MISMO style + checkpoint
            + (lora) a todos los retratos del set para coherencia visual.
            keyword-only.
        ref_face: nombre de una imagen de rostro de referencia en el directorio
            input/ del servidor ComfyUI. Si se pasa, encadena IPAdapter-FaceID para
            que el retrato tenga el MISMO rostro (identidad consistente entre
            retratos). None = identidad solo por prompt + seed. keyword-only.
        checkpoint: checkpoint del servidor. 'IMG_dreamshaper_8.safetensors' (SD1.5,
            holgado en 8GB lowvram) por defecto. FaceID solo esta instalado para
            SD1.5: si usas un checkpoint SDXL, deja ref_face=None. keyword-only.
        size: lado del cuadrado en px (width = height = size). 512 SD1.5 por
            defecto, encuadre tipico de avatar. keyword-only.
        facedetailer: si True encadena FaceDetailer (Impact-Pack) para regenerar la
            cara con detalle y descarta el SaveImage base (el unico PNG guardado es
            el refinado). False deja la imagen tal cual sale del VAEDecode.
            keyword-only.
        seed: semilla del KSampler (y del sampler del FaceDetailer). Misma seed +
            mismo character/ref_face -> mismo retrato. keyword-only.
        lora: LoRA de estilo opcional en models/loras (ej.
            'SD15_anime_lineart.safetensors'). None = sin LoRA. keyword-only.
        lora_strength: fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0].
            keyword-only.
        weight: peso del IPAdapter-FaceID (solo si ref_face). 0.85 = parecido alto;
            baja para mas libertad del prompt, sube para mas parecido. keyword-only.
        negative: prompt negativo. None usa el negativo por defecto pensado para
            retratos (una cara, bien formada, sin texto/recorte). keyword-only.
        steps: pasos del KSampler. keyword-only.
        cfg: CFG del KSampler (y del sampler del FaceDetailer). keyword-only.
        sampler_name: sampler del KSampler. keyword-only.
        scheduler: scheduler del KSampler. keyword-only.
        fd_denoise: fuerza de re-difusion de la cara en el FaceDetailer (0.45 por
            defecto: refina sin perder la identidad que aporta FaceID). Solo se usa
            si facedetailer=True. keyword-only.
        bbox_model: modelo de deteccion de caras de Ultralytics para el
            FaceDetailer ('face_yolov8m.pt'). Solo si facedetailer=True.
            keyword-only.
        filename_prefix: prefijo del PNG generado en output/. keyword-only.

    Returns:
        dict en API format listo para comfyui_submit_workflow: base txt2img (o
        IPAdapter-FaceID si ref_face) con prompt scaffold de retrato + LoRA de
        estilo opcional + FaceDetailer (si facedetailer). Es UN retrato; un set
        coherente -> llamar por character con el mismo style/checkpoint/(lora) y,
        para el mismo personaje, el mismo ref_face/seed.

    Raises:
        ValueError: si character esta vacio, o si la base/inyectores no encuentran
            los nodos donde enganchar (propagado por los builders que compone).
    """
    from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow

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

    character = character.strip()
    lora_strength = max(0.0, min(2.0, float(lora_strength)))
    neg = _PORTRAIT_NEGATIVE if negative is None else negative

    # Prompt scaffold de retrato: busto centrado, cara al espectador, fondo simple.
    positive = (
        f"portrait of {character}, {style}, bust shot, head and shoulders, "
        "looking at viewer, detailed face, sharp eyes, clean simple background, "
        "soft vignette, high detail"
    )

    if ref_face:
        from ml.comfyui_build_ipadapter_workflow import comfyui_build_ipadapter_workflow

        wf = comfyui_build_ipadapter_workflow(
            positive,
            ref_face,
            base_checkpoint=checkpoint,
            mode="faceid",
            weight=weight,
            negative=neg,
            steps=steps,
            cfg=cfg,
            width=size,
            height=size,
            seed=seed,
            sampler_name=sampler_name,
            scheduler=scheduler,
            filename_prefix=filename_prefix,
        )
    else:
        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,
        )

    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
        )

    if facedetailer:
        from ml.comfyui_build_facedetailer_workflow import (
            comfyui_build_facedetailer_workflow,
        )

        # Descarta el SaveImage base: el FaceDetailer toma la imagen del VAEDecode y
        # anhade su propio SaveImage (fd_save). Asi el unico PNG guardado es el de la
        # cara ya refinada (sin doble guardado). dict-comprehension = copia, no muta.
        wf = {k: v for k, v in wf.items() if v.get("class_type") != "SaveImage"}

        # Prompt del detailer enfocado en la cara, conservando personaje + estilo.
        fd_positive = f"{character}, {style}, detailed face, sharp eyes, skin texture"
        wf = comfyui_build_facedetailer_workflow(
            wf,
            checkpoint,
            fd_positive,
            neg,
            bbox_model=bbox_model,
            denoise=fd_denoise,
            steps=steps,
            cfg=cfg,
            seed=seed,
            sampler_name=sampler_name,
            scheduler=scheduler,
            filename_prefix=filename_prefix,
        )

    return wf


if __name__ == "__main__":
    import json

    wf = comfyui_build_portrait_avatar_workflow(
        "a young knight woman with red hair",
        style="realistic RPG portrait",
        facedetailer=True,
        seed=7,
    )
    print(
        json.dumps(
            {
                "nodes": list(wf),
                "classes": sorted({n["class_type"] for n in wf.values()}),
            },
            indent=2,
        )
    )
