"""comfyui_generate_styled_asset_oneshot — aplica un ESTILO a un asset de un solo tiro.

One-shot del grupo `gamedev-2d` que cierra el último hueco manual del sistema de
*style presets* (report 0190): antes, aplicar un estilo curado (gameboy / ghibli /
pixel-art-retro / ...) a un asset exigía encadenar A MANO cuatro pasos —
`comfyui_get_gamedev_style_preset` → `comfyui_apply_style_preset` → despachar al
builder del `kind` y generar → y, lo que más se olvidaba, llamar a mano al
post-proceso (`comfyui_pixelize_image`) que el preset declaraba. Este pipeline lo
promueve a UNA sola llamada y, sobre todo, **auto-aplica el post-proceso del preset**:
si el estilo pide pixelizar (gameboy, pixel-art-retro), el PNG sale ya pixelizado del
pipeline, sin paso manual. Es la doctrina del issue 0087 aplicada al estilo: el
registry no crece inflando builders ni el helper de presets, crece promoviendo a un
pipeline one-shot la composición repetida "elegir estilo → aplicarlo a un kind →
generar → post-procesar".

Flujo:

    1. comfyui_get_gamedev_style_preset(style_preset)   -> receta de estilo (datos puros)
    2. comfyui_apply_style_preset(preset, subject)       -> subject + builder_kwargs + size
                                                            + transparent + spec de post
    3. despacho kind -> builder atómico del registry (REUTILIZA el dispatch _SUPPORTED
       de comfyui_generate_asset_pack_oneshot, no se redefine el mapa kind->builder)
    4. comfyui_submit_workflow + comfyui_wait_result + comfyui_fetch_output_image
    5. AUTO-POST: si el preset declara post (p.ej. {"pixelize": {...}}), se aplica al
       PNG descargado con comfyui_pixelize_image -> la ruta FINAL ya es la post-procesada
    6. export opcional a un proyecto Godot (comfyui_export_asset_to_godot); si hubo
       pixelize, el asset se exporta como `pixelart` (filtro Nearest global en Godot)

A diferencia de `comfyui_generate_asset_pack_oneshot` (que comparte UN estilo libre
concatenado al subject de N assets variados) y de `comfyui_generate_character_set_oneshot`
(que produce las tres representaciones de UN personaje), este pipeline aplica UN preset
de estilo CURADO (con su checkpoint/LoRA/negativo/post coherentes) a UN asset concreto.
Reutiliza el mismo catálogo de `kind`s del pack, así que sirve para cualquiera de los 26
builders de sujeto del grupo.

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)

# --- presets de estilo (puros) ---
from ml.comfyui_apply_style_preset import comfyui_apply_style_preset
from ml.comfyui_get_gamedev_style_preset import comfyui_get_gamedev_style_preset

# --- post-proceso CPU (impuro: disco) ---
from ml.comfyui_pixelize_image import comfyui_pixelize_image

# --- 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

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

# REUTILIZA el dispatch kind -> (godot_kind, builder) del pack one-shot en vez de
# redefinir el mapa: una sola fuente de verdad de los `kind`s soportados y de a qué
# builder atómico va cada uno. _CKPT_PARAMS y _first_image también se comparten.
from pipelines.comfyui_generate_asset_pack_oneshot import (
    _CKPT_PARAMS,
    _SUPPORTED,
    _first_image,
    supported_kinds,
)


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

    Best-effort: en una RTX 3070 de 8 GB con modelos ya cargados de otra tarea, liberar
    antes de generar evita el OOM. 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_styled_workflow(
    kind: str,
    applied: dict,
    *,
    seed: int,
    builder_extra: dict,
) -> tuple[dict, str]:
    """Construye el workflow de UN asset con el estilo del preset ya aplicado.

    PURO (no toca la red): localiza el builder del `kind` en el dispatch compartido
    `_SUPPORTED`, introspecciona su firma y le pasa SOLO los kwargs que admita, tomados
    del resultado de `comfyui_apply_style_preset` (`applied`): el `subject` combinado, el
    `style`/`checkpoint`/`lora`/`lora_strength`/`negative` del estilo, y la `size`/
    `transparent` recomendadas. A diferencia del pack (que concatena un `style` libre al
    subject), aquí el `style` del preset SUSTITUYE el `style` categórico del builder, que
    es lo que da el look del estilo curado.

    Args:
        kind: tipo de asset (uno de `supported_kinds()`).
        applied: dict de `comfyui_apply_style_preset` ({subject, builder_kwargs, size,
            transparent, post, name}).
        seed: semilla del KSampler. keyword-only.
        builder_extra: kwargs extra del caller para posicionales requeridos por algunos
            builders (p.ej. `expression` en `emote`) o passthrough que el builder admita.
            keyword-only.

    Returns:
        (workflow, godot_kind): el dict API-format y el bucket Godot del builder
        (sprite/vfx/tileset) para el export.

    Raises:
        ValueError: si el `kind` no está soportado o falta un posicional requerido del
            builder (p.ej. `expression` en `emote`).
    """
    if kind not in _SUPPORTED:
        raise ValueError(
            f"kind {kind!r} no soportado. Soportados: {', '.join(supported_kinds())}"
        )
    godot_kind, fn = _SUPPORTED[kind]
    params = inspect.signature(fn).parameters
    bk = applied["builder_kwargs"]  # {style, checkpoint, lora, lora_strength, negative}

    kw: dict = {}
    # checkpoint -> el nombre de parámetro que use el builder (checkpoint/ckpt_name/ckpt).
    for cand in _CKPT_PARAMS:
        if cand in params:
            kw[cand] = bk["checkpoint"]
            break
    if "style" in params:
        kw["style"] = bk["style"]
    if "negative" in params:
        kw["negative"] = bk["negative"]
    if bk.get("lora") and "lora" in params:
        kw["lora"] = bk["lora"]
        if "lora_strength" in params:
            kw["lora_strength"] = bk["lora_strength"]
    if "size" in params and applied.get("size") is not None:
        kw["size"] = applied["size"]
    if "transparent" in params:
        kw["transparent"] = applied["transparent"]
    if "seed" in params:
        kw["seed"] = seed

    # Posicionales requeridos más allá del primero (= subject). Ej: emote.expression.
    pos_extra = [
        p.name
        for p in params.values()
        if p.default is inspect.Parameter.empty
        and p.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
    ][1:]
    pos_args = [applied["subject"]]
    for name in pos_extra:
        if name not in builder_extra:
            raise ValueError(
                f"kind {kind!r} requiere el campo {name!r}; pásalo como kwarg "
                f"(p.ej. {name}=...)"
            )
        pos_args.append(builder_extra[name])

    # Passthrough: cualquier otro kwarg extra del caller que el builder admita
    # (steps, cfg, rembg_model, width, height, glow, ...). Sobreescribe la coherencia.
    used = set(pos_extra)
    for key, val in builder_extra.items():
        if key in used:
            continue
        if key in params:
            kw[key] = val

    return fn(*pos_args, **kw), godot_kind


def styled_asset_build_only(
    kind: str,
    subject: str,
    style_preset: str,
    *,
    seed: int = 0,
    style_override: str | None = None,
    negative_extra: str | None = None,
    **builder_extra,
) -> dict:
    """Parte PURA del pipeline: resuelve el preset y construye el workflow, sin tocar la red.

    Útil para inspeccionar/testear el grafo y el post-proceso que el pipeline va a aplicar
    sin generar nada. Es lo que el pipeline hace en sus pasos 1-3.

    Returns:
        dict {kind, godot_kind, applied, workflow} donde `applied` es el resultado de
        `comfyui_apply_style_preset` (incluye `post`, la spec de post-proceso) y `workflow`
        es el dict API-format listo para `comfyui_submit_workflow`.

    Raises:
        ValueError: si el `kind` o el `style_preset` no existen, o si subject está vacío,
            o si falta un posicional requerido por el builder.
    """
    preset = comfyui_get_gamedev_style_preset(style_preset)  # ValueError si no existe
    applied = comfyui_apply_style_preset(
        preset, subject, style=style_override, negative=negative_extra
    )
    workflow, godot_kind = _build_styled_workflow(
        kind, applied, seed=seed, builder_extra=builder_extra
    )
    return {
        "kind": kind, "godot_kind": godot_kind, "applied": applied, "workflow": workflow,
    }


def comfyui_generate_styled_asset_oneshot(
    kind: str,
    subject: str,
    style_preset: str,
    *,
    seed: int = 0,
    server: str = "127.0.0.1:8188",
    out_dir: str | None = None,
    export_godot: str | None = None,
    style_override: str | None = None,
    negative_extra: str | None = None,
    wait_timeout: float = 600.0,
    free_vram: bool = False,
    godot_bin: str | None = None,
    **builder_extra,
) -> dict:
    """Genera UN asset aplicándole un estilo CURADO de un solo tiro, con auto-post-proceso.

    Toma un `kind` de asset (uno de `supported_kinds()`), un `subject` (qué dibujar) y el
    nombre de un `style_preset` del catálogo de `comfyui_get_gamedev_style_preset`
    ("gameboy", "ghibli", "pixel-art-retro", "cyberpunk-neon", "low-poly-flat",
    "cartoon-cel-shaded", ...), y produce el PNG final ya con el estilo aplicado Y su
    post-proceso ejecutado. Para los estilos que declaran post (gameboy, pixel-art-retro)
    el resultado sale ya pixelizado del pipeline — sin llamar a `comfyui_pixelize_image` a
    mano, que es el hueco que este one-shot cierra.

    Args:
        kind: tipo de asset; uno de `supported_kinds()` (item_icon, enemy_creature,
            prop_object, seamless_tile, ui_hud, ...). Un kind desconocido devuelve
            ok=False con error claro SIN tocar la GPU.
        subject: qué dibujar ("knight character", "health potion", "stone wall tile").
            Se combina con el prefijo/sufijo del estilo. Vacío -> error sin red.
        style_preset: nombre del estilo curado a aplicar. Insensible a mayúsculas y a
            '_' vs '-'. Un estilo desconocido devuelve ok=False con error claro (y la
            lista de estilos disponibles) SIN tocar la GPU.
        seed: semilla del KSampler para reproducibilidad. keyword-only.
        server: host:port del ComfyUI; se acepta con o sin esquema (http://). keyword-only.
        out_dir: directorio local donde descargar el PNG (crudo y post-procesado); None =
            un dir temporal por (estilo, seed). keyword-only.
        export_godot: ruta de un proyecto Godot 4; si se da, el asset FINAL (ya
            post-procesado) se exporta a res://assets/...; si hubo pixelize se exporta como
            `pixelart` (fija el filtro Nearest global). None = no exportar. keyword-only.
        style_override: sustituye el `style` del preset (override puntual); None usa el del
            preset. keyword-only.
        negative_extra: negativo extra del caller; se MERGEA con el del estilo (no lo
            reemplaza). keyword-only.
        wait_timeout: segundos máximos esperando el trabajo en ComfyUI. keyword-only.
        free_vram: si True hace POST /free ANTES de generar para liberar VRAM de tareas
            previas (útil en 8 GB con modelos ya cargados). keyword-only.
        godot_bin: binario de Godot para el reimport headless; None autodetecta. keyword-only.
        **builder_extra: kwargs extra para el builder del `kind` (posicionales requeridos
            como `expression` en `emote`, o passthrough como `steps`/`cfg`/`width`/`height`).

    Returns:
        dict con:
        - ok (bool): True si el asset se generó (y, si el estilo pedía post, el post se
          aplicó). Un fallo de post o de export se anota en `error` pero el crudo queda
          en `raw_path`.
        - kind, subject, style_preset, seed (eco de la entrada; `subject` es el combinado).
        - prompt_id (str): id del trabajo en ComfyUI.
        - raw_path (str): PNG descargado SIN post-procesar.
        - path (str): PNG FINAL entregable (post-procesado si el estilo lo pedía; si no,
          igual a raw_path).
        - post (dict): la spec de post del estilo ({} si no pedía).
        - post_applied (dict|None): resultado del post-proceso si se aplicó
          ({kind, out_path, n_colors_final, size} en éxito; {kind, ok:False, error} si
          falló); None si el estilo no pedía post.
        - size (int), transparent (bool): los que el estilo recomendó.
        - godot_kind (str): bucket Godot usado (sprite/vfx/tileset, o pixelart si hubo post).
        - exported (dict|None): resultado de export a Godot, o None.
        - coherence_note (str), error (str).
    """
    server = server.replace("http://", "").replace("https://", "").strip("/")
    out: dict = {
        "ok": False, "kind": kind, "subject": subject, "style_preset": style_preset,
        "seed": seed, "prompt_id": "", "raw_path": "", "path": "", "post": {},
        "post_applied": None, "size": None, "transparent": None, "godot_kind": "",
        "exported": None, "coherence_note": "", "error": "",
    }

    # --- 1-3. resolver estilo + construir workflow (PURO; valida kind/estilo sin red) ---
    try:
        built = styled_asset_build_only(
            kind, subject, style_preset, seed=seed,
            style_override=style_override, negative_extra=negative_extra,
            **builder_extra,
        )
    except ValueError as exc:
        out["error"] = str(exc)
        return out

    applied = built["applied"]
    workflow = built["workflow"]
    out["subject"] = applied["subject"]
    out["size"] = applied.get("size")
    out["transparent"] = applied.get("transparent")
    out["post"] = applied.get("post", {})
    out["godot_kind"] = built["godot_kind"]
    out["coherence_note"] = (
        f"Estilo {applied['name']!r} aplicado a un {kind!r}: "
        f"checkpoint={applied['builder_kwargs']['checkpoint']!r}, "
        f"lora={applied['builder_kwargs']['lora']!r}, "
        f"post={'sí' if out['post'] else 'no'} (auto-aplicado por el pipeline)."
    )

    asset_dir = out_dir or os.path.join(
        tempfile.gettempdir(), f"styled_asset_{applied['name']}_seed{seed}"
    )
    os.makedirs(asset_dir, exist_ok=True)

    if free_vram:
        _free_vram(server)

    # --- 4. encolar + esperar + descargar ---
    try:
        sub = comfyui_submit_workflow(workflow, server=server)
        out["prompt_id"] = sub["prompt_id"]
    except (RuntimeError, KeyError) as exc:
        out["error"] = f"submit falló: {exc}"
        return out

    try:
        outputs = comfyui_wait_result(out["prompt_id"], server=server, timeout=wait_timeout)
    except (TimeoutError, RuntimeError) as exc:
        out["error"] = f"wait falló: {exc}"
        return out

    img = _first_image(outputs)
    if img is None:
        out["error"] = f"el workflow no produjo imágenes (outputs={list(outputs)})"
        return out

    fetched = comfyui_fetch_output_image(
        img["filename"], subfolder=img.get("subfolder", ""),
        type_=img.get("type", "output"), server=server, dest_dir=asset_dir,
    )
    if not fetched.get("ok"):
        out["error"] = f"fetch falló: {fetched.get('error')}"
        return out
    out["raw_path"] = fetched["path"]
    out["path"] = fetched["path"]  # por defecto el crudo es el final (estilos sin post)

    # --- 5. AUTO-POST: aplica el post-proceso que el estilo declaró ---
    # Hoy el único post soportado es `pixelize`; el dispatch es extensible (un estilo
    # nuevo con otro post añade su rama aquí). Si el estilo no pide post, raw == final.
    post_ok = True
    pixelize_spec = out["post"].get("pixelize") if isinstance(out["post"], dict) else None
    if pixelize_spec:
        base, ext = os.path.splitext(os.path.basename(out["raw_path"]))
        dst = os.path.join(asset_dir, f"{base}_{applied['name']}{ext or '.png'}")
        pr = comfyui_pixelize_image(out["raw_path"], dst, **pixelize_spec)
        if pr.get("ok"):
            out["path"] = pr["out_path"]
            out["post_applied"] = {
                "kind": "pixelize", "out_path": pr["out_path"],
                "n_colors_final": pr["n_colors_final"], "size": pr["size"],
            }
        else:
            post_ok = False
            out["post_applied"] = {"kind": "pixelize", "ok": False, "error": pr.get("error")}
            out["error"] = f"post pixelize falló (asset crudo OK en raw_path): {pr.get('error')}"

    # --- 6. export opcional a Godot (asset FINAL ya post-procesado) ---
    if export_godot and out["path"]:
        # Si se pixelizó, exportar como `pixelart` asegura el filtro Nearest global en Godot.
        godot_kind = "pixelart" if out["post_applied"] and out["post_applied"].get("out_path") \
            else out["godot_kind"]
        exp = comfyui_export_asset_to_godot(
            out["path"], godot_kind, export_godot, reimport=True, godot_bin=godot_bin,
        )
        out["exported"] = exp
        if not exp.get("ok"):
            out["error"] = (out["error"] + "; " if out["error"] else "") + \
                f"export falló (asset generado igual): {exp.get('error')}"

    out["ok"] = bool(out["path"]) and post_ok
    return out


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


if __name__ == "__main__":
    res = comfyui_generate_styled_asset_oneshot(
        "item_icon", "health potion", "gameboy", seed=7, out_dir="/tmp/styled_asset_demo",
    )
    print(json.dumps(res, indent=2, ensure_ascii=False))
