"""comfyui_pixelize_sprite_png — pixeliza un PNG existente a un sprite pixel-art REAL.

Toma un PNG YA renderizado a alta resolucion (p.ej. 512x768 RGBA con fondo
transparente) y lo convierte en un sprite pixel-art de verdad de `size` x `size`.
Es la pieza reutilizable que extrae la logica de pixelizado de un PNG existente: la
MISMA secuencia que `comfyui_pixelart_real_oneshot` aplica internamente en sus fases
1b/2a/2a-bis/2b, pero desacoplada de la generacion. Sirve para pixelizar cada frame
de una animacion, una hoja de sprites, o cualquier render existente sin volver a
pasar por la difusion.

El metodo (report 0215) tiene tres etapas de post-proceso encadenadas:

  1. crop al contenido (`crop_to_content`): recorta al bounding box del sujeto y lo
     cuadra para que llene el frame; si el sujeto ocupa el 25% del lienzo, a 32px
     quedaria diminuto. Best-effort: si falla, se sigue con el PNG original.
  2. downscale a un grid `size` x `size`:
       - engine="pixeloe": downscale contrast-aware (`pixeloe_downscale`, no_upscale)
         que conserva la silueta — para sujetos con contorno (personajes, iconos).
         Si la lib no esta o falla, cae automaticamente a "nearest".
       - engine="nearest": downscale nearest simple (PIL) — mas barato, para
         tiles/texturas sin contorno.
     PixelOE trabaja en RGB y pierde el alpha, asi que tras el (fase 2a-bis) se
     downscalea el canal alpha por separado (nearest) y se reaplica al grid.
  3. cuantizacion dura (`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 -> N colores + 100% grid duro, preservando el alpha.

Compone funciones del registry, no reescribe su logica:
    crop_to_content_py_ml          (autocrop al contenido + cuadrar; pura)
    pixeloe_downscale_py_ml        (downscale contrast-aware, engine pixeloe)
    comfyui_pixelize_image_py_ml   (cuantizacion dura + alpha-aware)

Funcion impura: lectura/escritura de disco (+ subprocess del bridge de pixeloe).
No-throw: cualquier fallo se captura y viaja en el campo `error` del dict.
"""

from __future__ import annotations

import os
import sys
import tempfile

# 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_pixelize_image import comfyui_pixelize_image
from ml.crop_to_content import crop_to_content
from ml.pixeloe_downscale import pixeloe_downscale


def comfyui_pixelize_sprite_png(
    src_path: str,
    dst_path: str,
    *,
    size: int = 32,
    colors: int = 16,
    engine: str = "pixeloe",
    palette=None,
    transparent: bool = True,
    autocrop: bool = True,
    crop_pad_ratio: float = 0.02,
    mode: str = "contrast",
    patch_size: int = 16,
    thickness: int = 2,
    alpha_threshold: int = 128,
    comfy_python: str | None = None,
) -> dict:
    """Pixeliza un PNG existente a un sprite pixel-art REAL de `size` x `size`.

    Args:
        src_path: ruta del PNG de entrada (un render a alta resolucion, p.ej.
            512x768 RGBA con fondo transparente). Debe existir.
        dst_path: ruta del PNG de salida size x size (se crea el directorio si falta).
        size: lado del grid final en pixeles (32 iconos/objetos simples, 64
            personajes/sprites). Debe ser >= 1. 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 refleja 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.
        transparent: si True (default) trata la entrada como RGBA y produce un sprite
            RGBA con transparencia real (el fondo transparente no entra en la paleta).
            Para tiles/texturas opacas, False produce salida RGB. keyword-only.
        autocrop: si True (default) recorta el PNG al bounding box de su contenido y lo
            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 RGB.
            keyword-only.
        crop_pad_ratio: margen relativo que deja el autocrop alrededor del sujeto
            (0.02 = 2% del lado). 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.
        alpha_threshold: umbral (0..255) para binarizar el alpha en opaco (255) o
            transparente (0) en la cuantizacion final. Solo aplica si transparent.
            keyword-only.
        comfy_python: ruta al interprete de ComfyUI (con la lib pixeloe) para el
            bridge; None autodetecta COMFY_PYTHON y luego ~/ComfyUI/.venv/bin/python3.
            keyword-only.

    Returns:
        dict con:
        - ok (bool): True si se produjo el PNG final pixelizado.
        - out_path (str): ruta del PNG final size x size (vacio si fallo).
        - size (int): lado real del PNG final.
        - colors_final (int): numero de colores distintos en el resultado (en la zona
          opaca si es RGBA).
        - has_alpha (bool): True si el PNG final es RGBA con transparencia.
        - engine_used (str): "pixeloe" o "nearest" (refleja el fallback real).
        - autocrop_applied (bool): True si el autocrop recorto/cuadro la imagen.
        - error (str): mensaje de error; cadena vacia si todo OK.
    """
    out = {
        "ok": False,
        "out_path": "",
        "size": int(size),
        "colors_final": 0,
        "has_alpha": False,
        "engine_used": engine,
        "autocrop_applied": False,
        "error": "",
    }

    # --- Validaciones (no-throw). ---
    if not src_path or not os.path.isfile(src_path):
        out["error"] = f"src_path no existe: {src_path!r}"
        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

    # Directorio temporal para los intermedios (crop + mid); se limpia al final.
    try:
        tmp_dir = tempfile.mkdtemp(prefix="pixelize_sprite_")
    except OSError as exc:
        out["error"] = f"no se pudo crear directorio temporal: {exc}"
        return out

    crop_path = os.path.join(tmp_dir, "crop.png")
    mid_path = os.path.join(tmp_dir, "mid.png")

    try:
        # --- Fase 1b (opcional): autocrop al contenido + cuadrar. ---
        # La imagen sobre la que se hace el downscale: la recortada si autocrop, o la
        # original sin tocar.
        pre_ds_path = src_path
        if autocrop:
            try:
                from PIL import Image
                with Image.open(src_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 el src sin recortar.
                pre_ds_path = src_path
                out["autocrop_applied"] = False
                if not out["error"]:
                    out["error"] = f"autocrop fallo (no critico): {exc}"

        # --- Fase 2a: downscale a un grid `size` x `size` (mid). ---
        engine_used = engine
        if engine == "pixeloe":
            ds = pixeloe_downscale(
                pre_ds_path, mid_path, mode=mode, target_size=int(size),
                patch_size=int(patch_size), thickness=int(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 del 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. (engine="nearest" ya conserva su alpha, no hace falta.)
        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. ---
        quant = comfyui_pixelize_image(
            mid_path, dst_path, downscale=1, colors=int(colors), palette=palette,
            upscale_back=False, keep_alpha=bool(transparent),
            alpha_threshold=int(alpha_threshold),
        )
        if not quant.get("ok"):
            out["error"] = f"cuantizacion fallo: {quant.get('error')}"
            return out

        out["out_path"] = dst_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
        out["ok"] = True
        return out
    finally:
        # Limpieza de intermedios + directorio temporal.
        for tmp in (crop_path, mid_path):
            try:
                os.remove(tmp)
            except OSError:
                pass
        try:
            os.rmdir(tmp_dir)
        except OSError:
            pass


if __name__ == "__main__":
    import json

    # Demo autosuficiente: genera un PNG de prueba (circulo de color sobre fondo
    # transparente 512x512) y lo pixeliza a 32x32 con 16 colores y transparencia.
    demo_dir = tempfile.mkdtemp(prefix="pixelize_sprite_demo_")
    demo_src = os.path.join(demo_dir, "demo_src.png")
    demo_dst = os.path.join(demo_dir, "demo_sprite.png")

    try:
        from PIL import Image, ImageDraw
        im = Image.new("RGBA", (512, 512), (0, 0, 0, 0))  # fondo transparente
        draw = ImageDraw.Draw(im)
        draw.ellipse((96, 96, 416, 416), fill=(220, 60, 60, 255))   # circulo rojo
        draw.ellipse((176, 160, 256, 240), fill=(250, 230, 120, 255))  # ojo amarillo
        draw.ellipse((280, 160, 360, 240), fill=(60, 120, 220, 255))   # ojo azul
        im.save(demo_src)
    except ImportError as exc:
        print(json.dumps({"ok": False, "error": f"PIL no disponible: {exc}"}))
        raise SystemExit(0)

    res = comfyui_pixelize_sprite_png(
        demo_src, demo_dst, size=32, colors=16, engine="pixeloe",
        transparent=True, autocrop=True,
    )
    print(json.dumps(res, indent=2))
