"""comfyui_pixelart_real_oneshot — prompt de texto -> sprite pixel-art REAL en disco.

Pipeline one-shot (issue 0087) que materializa el metodo ganador de la
investigacion (report 0215): la difusion NO sabe pintar pixel-perfect (su salida
tiene decenas de miles de colores y bordes con anti-aliasing — pixel-art FALSO),
asi que el pixel-art de verdad es siempre post-proceso en dos ejes: colapsar a un
grid duro y limitar la paleta. El metodo ganador combina:

  1. Generar a alta resolucion con el look pixel-art (SDXL Juggernaut + LoRA
     SDXL_pixel-art), via comfyui_build_pixelart_workflow.
  2. Downscale contrast-aware con PixelOE (pixeloe_downscale): elige el pixel mas
     representativo de cada zona y engrosa contornos -> silueta legible. Es lo que
     distingue un sprite reconocible de una mancha. Solo para sujetos con silueta
     (engine="pixeloe"); para tiles/texturas sin contorno, un downscale nearest
     simple basta (engine="nearest") y es mas barato.
  3. Cuantizacion dura con comfyui_pixelize_image (downscale=1): clava la paleta
     exacta (N colores libres MEDIANCUT, o paleta fija pico-8 / nes / game-boy)
     sobre el grid ya hecho -> 16 colores exactos + 100% grid duro.

Resultado del combo verificado por PIL: grid duro perfecto + paleta limitada +
outline nitido. Sweet-spot: 64px personajes/sprites, 32px iconos/objetos simples.

Compone funciones del registry, no reescribe su logica:
    comfyui_build_pixelart_workflow_py_ml  (workflow SDXL + LoRA pixel-art)
    comfyui_submit_workflow_py_ml          (POST /prompt)
    comfyui_wait_result_py_ml              (poll /history)
    comfyui_fetch_output_image_py_ml       (GET /view -> disco, imagen base)
    pixeloe_downscale_py_ml                (downscale contrast-aware, engine pixeloe)
    comfyui_pixelize_image_py_ml           (cuantizacion dura + nearest fallback)

Pipeline impuro: red (HTTP a ComfyUI) + escritura en disco. No-throw: cualquier
fallo se captura y se devuelve en el dict de estado (campo error).
"""

from __future__ import annotations

import os
import sys

# Importa las funciones del registry (mismo arbol python/functions).
_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)

from ml.comfyui_build_pixelart_workflow import comfyui_build_pixelart_workflow
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
from ml.comfyui_pixelize_image import comfyui_pixelize_image
from ml.comfyui_submit_workflow import comfyui_submit_workflow
from ml.comfyui_wait_result import comfyui_wait_result
from ml.crop_to_content import crop_to_content
from ml.pixeloe_downscale import pixeloe_downscale

# Sufijo de encuadre: empuja al sujeto a llenar el frame para que tras el
# downscale conserve detalle por pixel (gotcha del report: un sujeto que ocupa el
# 25% del frame queda diminuto a 64px). Solo se anade si no esta ya presente.
_FRAME_HINT = "full body, centered, fills frame, no margins"


def _frame_subject(subject: str, fill_frame: bool) -> str:
    """Anade el hint de encuadre al subject si fill_frame y no esta ya."""
    if not fill_frame:
        return subject
    low = subject.lower()
    if "fills frame" in low or "full body" in low or "centered" in low:
        return subject
    return f"{subject}, {_FRAME_HINT}"


def comfyui_pixelart_real_oneshot(
    subject: str,
    *,
    size: int = 64,
    colors: int = 16,
    engine: str = "pixeloe",
    palette=None,
    server: str = "127.0.0.1:8188",
    dest_dir: str = "~/ComfyUI/output",
    seed: int = 0,
    negative: str | None = None,
    mode: str = "contrast",
    patch_size: int = 16,
    thickness: int = 2,
    fill_frame: bool = True,
    transparent: bool = True,
    autocrop: bool = True,
    crop_pad_ratio: float = 0.06,
    rembg_model: str = "u2net",
    upscale_preview: int = 512,
    keep_base: bool = True,
    comfy_python: str | None = None,
    wait_timeout: float = 300.0,
    filename_prefix: str = "pixelart_real",
    **gen_kwargs,
) -> dict:
    """Genera un sprite pixel-art REAL desde un prompt de texto, end-to-end.

    Args:
        subject: prompt positivo (lo que se quiere ver: "pixel art knight, full
            body, side view", etc.). No puede estar vacio.
        size: lado del grid final en pixeles (64 personajes/sprites, 32 iconos).
            keyword-only.
        colors: numero de colores de la paleta libre cuando palette es None
            (cuantizacion MEDIANCUT). keyword-only.
        engine: "pixeloe" (downscale contrast-aware, para sujetos con silueta:
            personajes/criaturas/iconos) o "nearest" (downscale nearest simple,
            mas barato, para tiles/texturas/fondos sin contorno). Si "pixeloe"
            falla o la lib no esta disponible, cae automaticamente a "nearest" y
            lo reporta en engine_used. keyword-only.
        palette: None (paleta libre a `colors`), nombre builtin ("pico-8", "nes",
            "game-boy") o lista de hex. Una paleta fija ignora `colors`.
            keyword-only.
        server: host:port del servidor ComfyUI (sin esquema). keyword-only.
        dest_dir: directorio donde guardar los PNG (se expande ~). keyword-only.
        seed: semilla del KSampler. keyword-only.
        negative: prompt negativo; None usa el default de build_pixelart
            (evita blur/gradientes/anti-alias). keyword-only.
        mode: modo de downscale de PixelOE ("contrast" SOTA, "k-centroid",
            "nearest", "center", "bicubic"); solo aplica con engine="pixeloe".
            keyword-only.
        patch_size: tamano de patch de PixelOE (default 16). keyword-only.
        thickness: grosor del outline expansion de PixelOE (default 2).
            keyword-only.
        fill_frame: si True, anade un hint de encuadre al subject para que el
            sujeto llene el frame (mejor detalle por pixel tras el downscale).
            keyword-only.
        transparent: si True (default) genera con fondo recortado (rembg en el
            workflow) y produce un sprite RGBA con transparencia real. Para
            tiles/texturas que NO quieren alpha, pasar transparent=False (el sprite
            sale RGB sobre fondo opaco). keyword-only.
        autocrop: si True (default) recorta la imagen base al bounding box de su
            contenido y la cuadra antes del downscale, para que el sujeto llene el
            frame (evita el sprite diminuto). Usa el alpha si transparent, o el color
            de fondo si no. keyword-only.
        crop_pad_ratio: margen relativo que deja el autocrop alrededor del sujeto
            (0.06 = 6% del lado). keyword-only.
        rembg_model: modelo Rembg para recortar el fondo ('u2net' general,
            'isnet-anime' para anime). Solo aplica si transparent. keyword-only.
        upscale_preview: si > 0, escribe ademas un PNG re-escalado nearest a
            ese lado (preview con pixeles duros, p.ej. 512). 0 lo desactiva.
            keyword-only.
        keep_base: si True conserva el PNG base de alta resolucion; si False lo
            borra tras pixelizar. keyword-only.
        comfy_python: ruta al interprete de ComfyUI (con la lib pixeloe); None
            autodetecta. keyword-only.
        wait_timeout: segundos maximos esperando al server. keyword-only.
        filename_prefix: prefijo de los archivos de salida. keyword-only.
        **gen_kwargs: params extra para comfyui_build_pixelart_workflow
            (width, height, ckpt_name, lora_strength, use_lcm, steps, cfg, ...).

    Returns:
        dict con:
        - ok (bool): True si se produjo el PNG final pixelizado.
        - out_path (str): ruta del PNG final size x size.
        - out_path_upscaled (str): ruta del preview re-escalado, o "" si off.
        - base_path (str): ruta del PNG base de alta resolucion (o "" si se borro).
        - size (int): lado real del PNG final.
        - colors_final (int): numero de colores distintos en el resultado (en la
          zona opaca si es RGBA).
        - engine_used (str): "pixeloe" o "nearest" (refleja el fallback).
        - has_alpha (bool): True si el PNG final es RGBA con transparencia.
        - autocrop_applied (bool): True si el autocrop recorto la imagen base.
        - prompt_id (str): id del trabajo en ComfyUI.
        - error (str): mensaje de error; vacio si OK.
    """
    out = {
        "ok": False, "out_path": "", "out_path_upscaled": "", "base_path": "",
        "size": int(size), "colors_final": 0, "engine_used": engine,
        "has_alpha": False, "autocrop_applied": False,
        "prompt_id": "", "error": "",
    }

    if not subject or not subject.strip():
        out["error"] = "subject vacio"
        return out
    if int(size) < 1:
        out["error"] = f"size debe ser >= 1, recibido {size!r}"
        return out
    if engine not in ("pixeloe", "nearest"):
        out["error"] = f"engine invalido: {engine!r} (usa 'pixeloe' o 'nearest')"
        return out

    dest = os.path.expanduser(dest_dir)
    try:
        os.makedirs(dest, exist_ok=True)
    except OSError as exc:
        out["error"] = f"no se pudo crear dest_dir {dest!r}: {exc}"
        return out

    # --- Fase 1: generar la imagen base de alta resolucion (look pixel-art) ---
    positive = _frame_subject(subject, fill_frame)
    try:
        if negative is None:
            workflow = comfyui_build_pixelart_workflow(
                positive, seed=seed, transparent=bool(transparent),
                rembg_model=rembg_model,
                filename_prefix=f"{filename_prefix}_base", **gen_kwargs,
            )
        else:
            workflow = comfyui_build_pixelart_workflow(
                positive, negative, seed=seed, transparent=bool(transparent),
                rembg_model=rembg_model,
                filename_prefix=f"{filename_prefix}_base", **gen_kwargs,
            )
    except (ValueError, TypeError) as exc:
        out["error"] = f"build workflow fallo: {exc}"
        return out

    try:
        sub = comfyui_submit_workflow(workflow, server=server)
        prompt_id = sub["prompt_id"]
        out["prompt_id"] = prompt_id
    except (RuntimeError, KeyError, OSError) as exc:
        out["error"] = f"submit fallo (server {server} responde?): {exc}"
        return out

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

    img = None
    for node_out in outputs.values():
        images = node_out.get("images") if isinstance(node_out, dict) else None
        if images:
            img = images[0]
            break
    if img is None:
        out["error"] = f"el workflow no produjo imagenes (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=dest,
    )
    if not fetched.get("ok"):
        out["error"] = f"fetch de imagen base fallo: {fetched.get('error')}"
        return out
    base_path = fetched["path"]
    out["base_path"] = base_path

    # --- Fase 1b (opcional): autocrop al contenido + cuadrar (sujeto llena el frame). ---
    # La imagen sobre la que se hace el downscale: la recortada si autocrop, o la base.
    pre_ds_path = base_path
    crop_path = ""
    if autocrop:
        crop_path = os.path.join(dest, f"{filename_prefix}_{size}px_crop.png")
        try:
            from PIL import Image
            with Image.open(base_path) as base_im:
                src_im = base_im.convert("RGBA") if transparent else base_im.convert("RGB")
                before = src_im.size
                cropped = crop_to_content(
                    src_im, pad_ratio=float(crop_pad_ratio), square=True,
                )
            cropped.save(crop_path)
            pre_ds_path = crop_path
            out["autocrop_applied"] = cropped.size != before
        except (ImportError, OSError, ValueError) as exc:
            # Autocrop es best-effort: si falla, se sigue con la base sin recortar.
            crop_path = ""
            pre_ds_path = base_path
            if not out["error"]:
                out["error"] = f"autocrop fallo (no critico): {exc}"

    # --- Fase 2a: downscale a un grid `size` x `size` (mid). ---
    mid_path = os.path.join(dest, f"{filename_prefix}_{size}px_mid.png")
    engine_used = engine

    if engine == "pixeloe":
        ds = pixeloe_downscale(
            pre_ds_path, mid_path, mode=mode, target_size=int(size),
            patch_size=patch_size, thickness=thickness, no_upscale=True,
            comfy_python=comfy_python,
        )
        if not ds.get("ok"):
            # Fallback limpio: PixelOE no disponible / fallo -> nearest.
            engine_used = "nearest"
            out["error"] = (
                f"pixeloe fallo ({ds.get('error')}); fallback a nearest"
            )

    if engine_used == "nearest":
        # Downscale nearest simple a size x size (PIL en el venv del registry).
        # nearest preserva el alpha por canal: si transparent, conserva la silueta.
        try:
            from PIL import Image
            with Image.open(pre_ds_path) as src:
                target_mode = "RGBA" if transparent else "RGB"
                small = src.convert(target_mode).resize(
                    (int(size), int(size)), Image.NEAREST
                )
                small.save(mid_path)
        except (ImportError, OSError) as exc:
            out["error"] = f"downscale nearest fallo: {exc}"
            return out

    if not os.path.isfile(mid_path):
        out["error"] = "no se genero la imagen intermedia (mid)"
        return out

    # --- Fase 2a-bis: recombinar alpha tras pixeloe (PixelOE trabaja en RGB). ---
    # El nucleo de PixelOE convierte a RGB: el grid `mid` sale sin transparencia. Se
    # downscalea el alpha de la imagen pre-downscale por separado (nearest al mismo
    # size) y se reaplica al grid para no perder el recorte ni la transparencia.
    if transparent and engine_used == "pixeloe":
        try:
            from PIL import Image
            with Image.open(pre_ds_path) as src_im:
                alpha = src_im.convert("RGBA").getchannel("A").resize(
                    (int(size), int(size)), Image.NEAREST
                )
            with Image.open(mid_path) as mid_im:
                mid_rgba = mid_im.convert("RGBA")
                mid_rgba.putalpha(alpha)
                mid_rgba.save(mid_path)
        except (ImportError, OSError) as exc:
            if not out["error"]:
                out["error"] = f"recombinacion de alpha fallo (no critico): {exc}"

    # --- Fase 2b: cuantizacion dura (paleta exacta) sobre el grid ya hecho. ---
    final_tag = palette if isinstance(palette, str) else f"q{colors}"
    final_path = os.path.join(
        dest, f"{filename_prefix}_{size}px_{engine_used}_{final_tag}.png"
    )
    quant = comfyui_pixelize_image(
        mid_path, final_path, downscale=1, colors=int(colors),
        palette=palette, upscale_back=False, keep_alpha=bool(transparent),
    )
    if not quant.get("ok"):
        out["error"] = f"cuantizacion fallo: {quant.get('error')}"
        return out

    out["out_path"] = final_path
    out["size"] = quant["size"][0] if quant.get("size") else int(size)
    out["colors_final"] = quant.get("n_colors_final", 0)
    out["has_alpha"] = bool(quant.get("has_alpha", False))
    out["engine_used"] = engine_used

    # --- Fase 3 (opcional): preview re-escalado nearest a pixeles duros. ---
    if int(upscale_preview) > 0:
        up_path = os.path.join(
            dest, f"{filename_prefix}_{size}px_{engine_used}_{final_tag}_up.png"
        )
        try:
            from PIL import Image
            with Image.open(final_path) as fin:
                prev_mode = "RGBA" if transparent else "RGB"
                up = fin.convert(prev_mode).resize(
                    (int(upscale_preview), int(upscale_preview)), Image.NEAREST
                )
                up.save(up_path)
            out["out_path_upscaled"] = up_path
        except (ImportError, OSError) as exc:
            # El preview es opcional: no invalida el resultado.
            out["out_path_upscaled"] = ""
            if not out["error"]:
                out["error"] = f"preview upscale fallo (no critico): {exc}"

    # Limpieza de intermedios (mid + crop temporal).
    for tmp in (mid_path, crop_path):
        if tmp:
            try:
                os.remove(tmp)
            except OSError:
                pass
    if not keep_base:
        try:
            os.remove(base_path)
            out["base_path"] = ""
        except OSError:
            pass

    out["ok"] = True
    return out


if __name__ == "__main__":
    import json

    res = comfyui_pixelart_real_oneshot(
        "pixel art knight, full body, centered, game sprite",
        size=64, colors=16, engine="pixeloe", seed=42,
        transparent=True, autocrop=True,
        dest_dir="/tmp/comfy_pixelart_real",
    )
    print(json.dumps(res, indent=2))
