"""comfyui_generate_character_set_oneshot — set COMPLETO y coherente de UN personaje.

Culminación del grupo `gamedev-2d`: las 5 fronteras de generación (crear 2D,
transformar, animar, 2.5D direccional SV3D, 3D-mesh Hunyuan3D) ya existen como
builders/pipelines sueltos, pero ninguna pieza resolvía la *coherencia
cross-frontera*: generar el set ENTERO de un personaje de juego (sprite 2D + sprite
direccional 8-way + malla 3D) exigía llamar 4 funciones a mano y NO garantizaba que
las tres representaciones fueran del MISMO personaje con el mismo estilo.

Este pipeline lo promueve a UNA sola llamada (doctrina issue 0087: el registry crece
promoviendo composiciones repetidas a pipelines one-shot, no inflando funciones). La
clave de la coherencia: la vista direccional y la malla 3D parten de la MISMA imagen
base 2D, no de tres generaciones independientes → es el mismo personaje fotografiado
de tres maneras, no tres personajes distintos.

Flujo (secuencial, liberando VRAM entre los pasos pesados para caber en 8 GB):

    1. base 2D (txt2img de un builder de personaje del catálogo, recortado a alpha)
       └─ comfyui_build_enemy_creature_workflow (o portrait_avatar / topdown_sprite)
          + comfyui_submit_workflow + comfyui_wait_result + comfyui_fetch_output_image
    2. aplanar la base sobre fondo sólido (los modelos 3D/SV3D quieren fondo opaco)
       └─ comfyui_flatten_alpha_on_color
    3. malla 3D desde la base aplanada
       └─ comfyui_image_to_3d_oneshot  (sube la imagen + Hunyuan3D-2 + descarga .glb)
    4. sprite direccional N-way desde la MISMA base aplanada
       └─ comfyui_build_directional_sprite_workflow (SV3D/Zero123)
          + _upload_image + submit + wait + fetch (N frames del orbit)
    5. export opcional a un proyecto Godot
       └─ comfyui_export_asset_to_godot

Compone funciones del registry — no reescribe ninguna. Un fallo aislado (p.ej. OOM
en el 3D) NO aborta el resto del set: devuelve lo que sí salió + el error parcial.

Pipeline impuro: red (HTTP a ComfyUI) + escritura en disco + (export) subprocess.
"""
from __future__ import annotations

import inspect
import json
import os
import sys
import tempfile
import urllib.error
import urllib.request

_FUNCTIONS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _FUNCTIONS_ROOT not in sys.path:
    sys.path.insert(0, _FUNCTIONS_ROOT)

# --- builders de personaje (puros) — base 2D del set ---
from ml.comfyui_build_enemy_creature_workflow import comfyui_build_enemy_creature_workflow
from ml.comfyui_build_portrait_avatar_workflow import comfyui_build_portrait_avatar_workflow
from ml.comfyui_build_topdown_sprite_workflow import comfyui_build_topdown_sprite_workflow

# --- builder direccional 2.5D (puro) + helper de orden de vistas ---
from ml.comfyui_build_directional_sprite_workflow import (
    comfyui_build_directional_sprite_workflow,
    directional_sprite_view_order,
)

# --- post-proceso CPU: aplanar alpha sobre color sólido (entrada de SV3D/Hunyuan) ---
from ml.comfyui_flatten_alpha_on_color import comfyui_flatten_alpha_on_color

# --- transporte ComfyUI (impuro) ---
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
from ml.comfyui_submit_workflow import comfyui_submit_workflow
from ml.comfyui_wait_result import comfyui_wait_result

# --- pipeline 3D reutilizado entero (sube imagen + Hunyuan3D-2 + descarga .glb) ---
# y su helper de upload al input/ del servidor (lógica de transporte ya existente).
from pipelines.comfyui_image_to_3d_oneshot import (
    _upload_image,
    comfyui_image_to_3d_oneshot,
)

# --- export opcional a Godot ---
from pipelines.comfyui_export_asset_to_godot import comfyui_export_asset_to_godot

# base_kind -> builder de personaje. El primer posicional de cada builder es el
# nombre del personaje (creature/character/subject); el resto de coherencia
# (checkpoint/size/seed/transparent/lora/style) se pasa por introspección.
_BASE_BUILDERS: dict[str, object] = {
    "enemy_creature": comfyui_build_enemy_creature_workflow,
    "portrait_avatar": comfyui_build_portrait_avatar_workflow,
    "topdown_sprite": comfyui_build_topdown_sprite_workflow,
}

# Nombres de parámetro candidatos para el checkpoint, por orden de preferencia.
_CKPT_PARAMS = ("checkpoint", "ckpt_name", "ckpt")


def supported_base_kinds() -> list[str]:
    """Lista ordenada de los `base_kind` válidos para la imagen base 2D del set."""
    return sorted(_BASE_BUILDERS)


def _free_vram(server: str) -> bool:
    """Pide a ComfyUI que descargue modelos y libere VRAM (POST /free).

    Best-effort: entre el paso 3D (Hunyuan) y el direccional (SV3D), ambos con pico
    ~7 GB en una RTX 3070 de 8 GB, liberar la VRAM del paso anterior evita el OOM al
    encadenarlos. No lanza: si el endpoint falla, devuelve False y el caller sigue.
    """
    try:
        body = json.dumps({"unload_models": True, "free_memory": True}).encode()
        req = urllib.request.Request(
            f"http://{server}/free", data=body,
            headers={"Content-Type": "application/json"}, method="POST",
        )
        with urllib.request.urlopen(req, timeout=30) as resp:
            resp.read()
        return True
    except (urllib.error.URLError, OSError):
        return False


def _build_base_workflow(
    base_kind: str,
    character: str,
    *,
    checkpoint: str,
    style: str,
    size: int | None,
    seed: int,
    lora: str | None,
    lora_strength: float,
) -> dict:
    """Construye el workflow de la base 2D inyectando solo los kwargs que el builder admita.

    PURO. Pasa `transparent=True` si el builder lo soporta (la base recortada a alpha
    es el deliverable 2D y la fuente de los demás pasos). Introspección de firma para
    no pasar kwargs que el builder no acepte.
    """
    fn = _BASE_BUILDERS[base_kind]
    params = inspect.signature(fn).parameters
    kw: dict = {}
    for cand in _CKPT_PARAMS:
        if cand in params:
            kw[cand] = checkpoint
            break
    if "style" in params and style:
        kw["style"] = style
    if "size" in params and size is not None:
        kw["size"] = size
    if "seed" in params:
        kw["seed"] = seed
    if "transparent" in params:
        kw["transparent"] = True
    if lora and "lora" in params:
        kw["lora"] = lora
        if "lora_strength" in params:
            kw["lora_strength"] = lora_strength
    return fn(character, **kw)


def _first_image(outputs: dict) -> dict | None:
    """Primer descriptor de imagen {filename, subfolder, type} en los outputs."""
    for node_out in outputs.values():
        images = node_out.get("images") if isinstance(node_out, dict) else None
        if images:
            return images[0]
    return None


def _all_images(outputs: dict) -> list:
    """Todos los descriptores de imagen de los outputs, en orden de nodo y de batch.

    El SaveImage de SV3D emite los N frames del orbit como una lista en un único
    nodo; este helper los aplana para descargarlos todos (uno por dirección).
    """
    out: list = []
    for node_out in outputs.values():
        images = node_out.get("images") if isinstance(node_out, dict) else None
        if images:
            out.extend(images)
    return out


def comfyui_generate_character_set_oneshot(
    character: str,
    *,
    style: str = "game character, full body, clean background",
    checkpoint: str = "IMG_dreamshaper_8.safetensors",
    base_kind: str = "enemy_creature",
    directions: int = 8,
    make_directional: bool = True,
    make_3d: bool = True,
    directional_model: str = "sv3d",
    elevation: float = 15.0,
    seed: int = 0,
    size: int = 512,
    directional_size: int | None = None,
    flatten_color: tuple = (255, 255, 255),
    variant_3d: str = "mini",
    lora: str | None = None,
    lora_strength: float = 1.0,
    server: str = "http://127.0.0.1:8188",
    export_godot: str | None = None,
    out_dir: str | None = None,
    wait_timeout: float = 600.0,
    free_vram: bool = True,
    godot_bin: str | None = None,
) -> dict:
    """Genera el set COMPLETO y coherente de UN personaje de juego, de un solo tiro.

    Produce, del MISMO personaje y con el MISMO estilo: (1) una imagen base 2D
    recortada a alpha, (2) un sprite direccional N-way (vistas 3D consistentes) y
    (3) una malla 3D `.glb`. La (2) y la (3) parten de la MISMA base 2D aplanada, así
    que las tres representaciones son coherentes (mismo personaje, no tres distintos).

    Args:
        character: descripción del personaje ("armored paladin", "goblin warrior",
            "fire mage"). Se pasa al builder de la base 2D.
        style: estilo común de todo el set; el builder base lo concatena/usa según su
            firma. Es la firma visual compartida. keyword-only.
        checkpoint: modelo base de la generación 2D, compartido. keyword-only.
        base_kind: builder de personaje para la base 2D — uno de
            `supported_base_kinds()` ("enemy_creature" default, "portrait_avatar",
            "topdown_sprite"). keyword-only.
        directions: nº de direcciones del sprite direccional (8 = 8-way, 4 = RPG
            clásico). keyword-only.
        make_directional: si True genera el sprite direccional. keyword-only.
        make_3d: si True genera la malla 3D. keyword-only.
        directional_model: "sv3d" (orbit turntable, mejor consistencia) o "zero123"
            (batch, menor VRAM). keyword-only.
        elevation: picado de cámara del orbit direccional en grados (~15-30 para
            top-down/iso). keyword-only.
        seed: semilla compartida de la generación (base 2D + direccional). keyword-only.
        size: lado en px de la base 2D cuadrada. keyword-only.
        directional_size: lado de cada vista direccional; None = nativo del modelo
            (576 sv3d / 256 zero123). La base se aplana a este tamaño para alimentar
            el modelo 3D direccional. keyword-only.
        flatten_color: color RGB de fondo sobre el que se aplana la base recortada
            antes de los pasos 3D/direccional (blanco por defecto). keyword-only.
        variant_3d: variante Hunyuan3D-2 para la malla ("mini" default, "standard",
            "mv"). keyword-only.
        lora: LoRA de estilo compartido (solo si el builder base lo admite). keyword-only.
        lora_strength: fuerza del LoRA común. keyword-only.
        server: host:port del ComfyUI (acepta con o sin esquema http://). keyword-only.
        export_godot: ruta de un proyecto Godot 4; si se da, la base 2D (sprite) y la
            malla (model) se exportan a `res://assets/...`. None = no exportar. keyword-only.
        out_dir: directorio local de descarga; None = un dir temporal por personaje.
            keyword-only.
        wait_timeout: segundos máximos esperando cada trabajo en ComfyUI. keyword-only.
        free_vram: si True hace POST /free entre los pasos pesados (3D y direccional)
            para caber en 8 GB. keyword-only.
        godot_bin: binario de Godot para el reimport headless; None autodetecta. keyword-only.

    Returns:
        dict {ok, character, style, checkpoint, base_kind, seed, coherence_note,
        base_image, base_flat, base_prompt_id, directional, mesh, exported, steps,
        error}. `base_image` = PNG RGBA recortado (deliverable 2D); `base_flat` = PNG
        aplanado que alimenta los pasos 3D/direccional (la fuente común que garantiza
        la coherencia); `directional` = {ok, model, directions, view_order, views:
        [{direction, path}], prompt_id, error}; `mesh` = {ok, mesh_glb, faces,
        prompt_id, error}; `exported` = lista de resultados de export a Godot. `ok`
        global es True si la base salió y todos los pasos solicitados
        (make_3d/make_directional) tuvieron éxito. Un paso solicitado que falla deja
        el set PARCIAL (su sub-dict con ok=False y error), pero NO aborta los demás.
    """
    server = server.replace("http://", "").replace("https://", "").strip("/")
    out: dict = {
        "ok": False, "character": character, "style": style, "checkpoint": checkpoint,
        "base_kind": base_kind, "seed": seed, "coherence_note": "",
        "base_image": "", "base_flat": "", "base_prompt_id": "",
        "directional": None, "mesh": None, "exported": [], "steps": [], "error": "",
    }

    def _step(name: str, ok: bool, detail: str = "") -> None:
        out["steps"].append({"step": name, "ok": ok, "detail": detail})

    # --- validación temprana (antes de tocar la GPU) ---
    if not character or not character.strip():
        out["error"] = "character vacío"
        return out
    if base_kind not in _BASE_BUILDERS:
        out["error"] = (
            f"base_kind {base_kind!r} no soportado. "
            f"Soportados: {', '.join(supported_base_kinds())}"
        )
        return out
    if not make_3d and not make_directional:
        out["error"] = "nada que generar: make_3d y make_directional ambos False"
        return out

    char_dir = out_dir or os.path.join(
        tempfile.gettempdir(),
        f"character_set_{character.strip().replace(' ', '_')}_seed{seed}",
    )
    os.makedirs(char_dir, exist_ok=True)
    out["coherence_note"] = (
        f"Set coherente de {character!r}: la base 2D (base_kind={base_kind!r}, "
        f"checkpoint={checkpoint!r}, style={style!r}, seed={seed}) se aplana una vez "
        f"y de esa MISMA base derivan el sprite direccional y la malla 3D — mismo "
        f"personaje en las tres representaciones, no tres generaciones independientes."
    )

    # --- 1. base 2D ---
    try:
        base_wf = _build_base_workflow(
            base_kind, character.strip(), checkpoint=checkpoint, style=style,
            size=size, seed=seed, lora=lora, lora_strength=lora_strength,
        )
        sub = comfyui_submit_workflow(base_wf, server=server)
        out["base_prompt_id"] = sub["prompt_id"]
        outputs = comfyui_wait_result(sub["prompt_id"], server=server, timeout=wait_timeout)
        img = _first_image(outputs)
        if img is None:
            out["error"] = "la base 2D no produjo imágenes"
            _step("base_2d", False, out["error"])
            return out
        fetched = comfyui_fetch_output_image(
            img["filename"], subfolder=img.get("subfolder", ""),
            type_=img.get("type", "output"), server=server, dest_dir=char_dir,
        )
        if not fetched.get("ok"):
            out["error"] = f"fetch de la base falló: {fetched.get('error')}"
            _step("base_2d", False, out["error"])
            return out
        out["base_image"] = fetched["path"]
        _step("base_2d", True, out["base_image"])
    except (ValueError, TypeError, RuntimeError, KeyError, TimeoutError) as exc:
        out["error"] = f"base 2D falló: {exc}"
        _step("base_2d", False, out["error"])
        return out

    # --- 2. aplanar la base sobre fondo sólido (entrada de los modelos 3D) ---
    flat_size = directional_size  # None => el flatten conserva tamaño; se reescala abajo
    flat = comfyui_flatten_alpha_on_color(
        out["base_image"],
        out_path=os.path.join(char_dir, "base_flat.png"),
        color=tuple(flatten_color),
        size=flat_size,
    )
    if not flat.get("ok"):
        out["error"] = f"flatten de la base falló: {flat.get('error')}"
        _step("flatten", False, out["error"])
        return out
    out["base_flat"] = flat["out_path"]
    _step("flatten", True, out["base_flat"])

    # --- 3. malla 3D desde la base aplanada (paso pesado #1) ---
    if make_3d:
        mesh_dir = os.path.join(char_dir, "mesh")
        os.makedirs(mesh_dir, exist_ok=True)  # dir existente => fetch nombra el .glb dentro
        mesh = comfyui_image_to_3d_oneshot(
            out["base_flat"], server=server, variant=variant_3d,
            dest=mesh_dir, wait_timeout=wait_timeout, seed=seed,
        )
        out["mesh"] = {
            "ok": mesh.get("ok", False), "mesh_glb": mesh.get("mesh_path", ""),
            "faces": mesh.get("faces"), "prompt_id": mesh.get("prompt_id", ""),
            "error": mesh.get("error", ""),
        }
        _step("mesh_3d", out["mesh"]["ok"], out["mesh"]["mesh_glb"] or out["mesh"]["error"])
        if free_vram and make_directional:
            _step("free_vram", _free_vram(server), "tras 3D, antes del direccional")

    # --- 4. sprite direccional desde la MISMA base aplanada (paso pesado #2) ---
    if make_directional:
        directional: dict = {
            "ok": False, "model": directional_model, "directions": directions,
            "view_order": directional_sprite_view_order(directions), "views": [],
            "prompt_id": "", "error": "",
        }
        try:
            server_name = _upload_image(out["base_flat"], server)
            dir_wf = comfyui_build_directional_sprite_workflow(
                server_name, directions=directions, model=directional_model,
                elevation=elevation, size=directional_size, seed=seed,
            )
            sub = comfyui_submit_workflow(dir_wf, server=server)
            directional["prompt_id"] = sub["prompt_id"]
            outputs = comfyui_wait_result(sub["prompt_id"], server=server, timeout=wait_timeout)
            imgs = _all_images(outputs)
            if not imgs:
                directional["error"] = "el direccional no produjo imágenes"
            else:
                names = directional["view_order"]
                for i, im in enumerate(imgs):
                    label = names[i] if i < len(names) else f"frame{i}"
                    got = comfyui_fetch_output_image(
                        im["filename"], subfolder=im.get("subfolder", ""),
                        type_=im.get("type", "output"), server=server,
                        dest_dir=os.path.join(char_dir, "directional"),
                    )
                    if got.get("ok"):
                        directional["views"].append({"direction": label, "path": got["path"]})
                directional["ok"] = len(directional["views"]) > 0
                if not directional["ok"]:
                    directional["error"] = "no se pudo descargar ninguna vista direccional"
        except (ValueError, TypeError, RuntimeError, KeyError, TimeoutError) as exc:
            directional["error"] = f"direccional falló: {exc}"
        out["directional"] = directional
        _step("directional", directional["ok"],
              f"{len(directional['views'])}/{directions} vistas" if directional["ok"]
              else directional["error"])

    # --- 5. export opcional a Godot (base sprite + malla) ---
    if export_godot:
        if out["base_image"]:
            exp = comfyui_export_asset_to_godot(
                out["base_image"], "sprite", export_godot,
                reimport=not (make_3d and out.get("mesh", {}).get("ok")),
                godot_bin=godot_bin,
            )
            out["exported"].append({"asset": "base_image", "result": exp})
        if make_3d and out.get("mesh") and out["mesh"]["ok"] and out["mesh"]["mesh_glb"]:
            exp = comfyui_export_asset_to_godot(
                out["mesh"]["mesh_glb"], "model", export_godot,
                reimport=True, godot_bin=godot_bin,
            )
            out["exported"].append({"asset": "mesh", "result": exp})

    # --- veredicto global: base OK + todos los pasos solicitados OK ---
    ok = bool(out["base_image"])
    if make_3d:
        ok = ok and bool(out.get("mesh") and out["mesh"]["ok"])
    if make_directional:
        ok = ok and bool(out.get("directional") and out["directional"]["ok"])
    out["ok"] = ok
    if not ok and not out["error"]:
        parts = []
        if make_3d and out.get("mesh") and not out["mesh"]["ok"]:
            parts.append(f"3D: {out['mesh']['error']}")
        if make_directional and out.get("directional") and not out["directional"]["ok"]:
            parts.append(f"direccional: {out['directional']['error']}")
        out["error"] = "set PARCIAL — " + "; ".join(parts) if parts else "set parcial"
    return out


# Alias con el nombre completo del ID para descubrimiento por convención.
generate_character_set_oneshot = comfyui_generate_character_set_oneshot


if __name__ == "__main__":
    res = comfyui_generate_character_set_oneshot(
        "armored paladin",
        style="dark fantasy, hand-painted, full body, clean background",
        base_kind="enemy_creature",
        directions=8,
        make_3d=True,
        make_directional=True,
        seed=7,
        size=512,
        out_dir="/tmp/character_set_demo",
    )
    print(json.dumps(res, indent=2, ensure_ascii=False))
