"""comfyui_generate_with_skill_oneshot — skill + subject → PNG juzgado en una llamada.

Cierra el bucle "habilidades de generación cada vez mejores" del grupo `comfyui-skill`:
carga una receta de skill, la compila para un *subject* concreto, encola, espera, descarga
el PNG y (si ``judge=True``) lo puntúa con el panel `comfyui-judge`, acumulando el score en
la media de la skill. Es la promoción a one-shot de la secuencia repetida
load → build → submit → wait → fetch → judge (issue 0087).

Compone funciones del registry:

    comfyui_load_skill_py_ml             (lee recipe.json)
    comfyui_build_skill_workflow_py_ml   (receta + subject → workflow API format, PURA)
    comfyui_submit_workflow_py_ml        (POST /prompt)
    comfyui_wait_result_py_ml            (poll /history)
    comfyui_fetch_output_image_py_ml     (GET /view → disco)
    comfyui_judge_image_py_ml            (panel multi-juez: estético + CLIP + LLM-vision)
    comfyui_update_skill_score_py_ml     (media incremental de score_mean/score_n)

Pipeline impuro: red (HTTP) + escritura en disco + (si juzga) API Anthropic.

`recipe_patch` permite probar una **variante** en memoria sin guardarla: se aplica a la
receta cargada antes de compilar. Cuando se pasa un patch, el score juzgado NO se acumula en
``score_mean`` (es una prueba, no la receta canónica): el caller decide si promover la
variante con `comfyui_bump_skill_version`. Sin patch, el score sí alimenta la media de la skill.
"""

from __future__ import annotations

import os
import sys

# Importa las funciones del registry (mismo árbol 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_skill_workflow import build_skill_workflow, SkillWorkflowError
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
from ml.comfyui_judge_image import comfyui_judge_image
from ml.comfyui_load_skill import comfyui_load_skill
from ml.comfyui_submit_workflow import comfyui_submit_workflow
from ml.comfyui_update_skill_score import comfyui_update_skill_score
from ml.comfyui_wait_result import comfyui_wait_result


def _deep_merge(base: dict, patch: dict) -> dict:
    """Merge recursivo de ``patch`` sobre ``base`` sin mutar los originales."""
    out = dict(base)
    for key, val in (patch or {}).items():
        if isinstance(val, dict) and isinstance(out.get(key), dict):
            out[key] = _deep_merge(out[key], val)
        else:
            out[key] = val
    return out


def _resolve_prompt(recipe: dict, subject: str) -> str:
    """Reconstruye el prompt positivo resuelto (para pasárselo al juez de fidelidad)."""
    scaffold = recipe.get("prompt_scaffold") or {}
    positive = str(scaffold.get("positive", "") or "")
    if "{subject}" in positive:
        positive = positive.replace("{subject}", subject)
    elif not positive:
        positive = subject
    else:
        positive = f"{subject}, {positive}"
    triggers = scaffold.get("trigger_words") or []
    if triggers:
        positive = ", ".join(list(triggers) + [positive]) if positive else ", ".join(triggers)
    return positive


def comfyui_generate_with_skill_oneshot(
    slug: str,
    subject: str,
    *,
    server: str = "127.0.0.1:8188",
    dest: str | None = None,
    seed: int = 0,
    judge: bool = True,
    recipe_patch: dict | None = None,
    library_dir: str | None = None,
    wait_timeout: float = 600.0,
) -> dict:
    """Genera (y opcionalmente juzga) una imagen a partir de una skill, end-to-end.

    Args:
        slug: slug de la skill a usar (su carpeta en la librería).
        subject: sujeto concreto que sustituye ``{subject}`` en el scaffold del prompt
            (p.ej. "a woman with red hair").
        server: host:port del servidor ComfyUI (sin esquema). keyword-only.
        dest: directorio local donde guardar el PNG (None = cwd). keyword-only.
        seed: semilla de generación. keyword-only.
        judge: si True, puntúa el PNG con el panel `comfyui-judge` y acumula el score en la
            skill (salvo que se pase ``recipe_patch``). keyword-only.
        recipe_patch: dict de cambios aplicados a la receta EN MEMORIA antes de compilar
            (deep-merge), para probar una variante sin guardarla. Con patch, el score NO se
            acumula en ``score_mean``. keyword-only.
        library_dir: raíz de la librería. Default ``~/ComfyUI/skills_library``. keyword-only.
        wait_timeout: segundos máximos esperando al servidor. keyword-only.

    Returns:
        dict ``{ok, slug, prompt_id, image_path, prompt_resolved, judge, judge_run_id,
        score_mean, score_n, error}``. ``judge`` = ``{verdict, score, votes}`` (o None si
        ``judge=False`` o el panel falla). ``judge_run_id`` correlaciona el juicio (sirve de
        evidencia para `comfyui_bump_skill_version`). Si falla, ``ok=False`` y ``error``
        explica en qué paso.
    """
    base = {"ok": False, "slug": slug, "prompt_id": "", "image_path": "",
            "prompt_resolved": "", "judge": None, "judge_run_id": "",
            "score_mean": None, "score_n": None, "error": ""}

    # 1. Cargar la receta de la skill.
    loaded = comfyui_load_skill(slug, library_dir=library_dir)
    if not loaded.get("ok"):
        return {**base, "error": f"load_skill falló: {loaded.get('error')}"}
    recipe = loaded["recipe"]
    if recipe_patch:
        recipe = _deep_merge(recipe, recipe_patch)

    prompt_resolved = _resolve_prompt(recipe, subject)

    # 2. Compilar la receta a un workflow (función pura del registry).
    try:
        workflow = build_skill_workflow(recipe, subject, seed=seed)
    except SkillWorkflowError as exc:
        return {**base, "prompt_resolved": prompt_resolved,
                "error": f"build_skill_workflow falló: {exc}"}

    # 3. Encolar.
    try:
        sub = comfyui_submit_workflow(workflow, server=server)
        prompt_id = sub["prompt_id"]
    except (RuntimeError, KeyError) as exc:
        return {**base, "prompt_resolved": prompt_resolved,
                "error": f"submit falló: {exc}"}

    # 4. Esperar a que termine.
    try:
        outputs = comfyui_wait_result(prompt_id, server=server, timeout=wait_timeout)
    except (TimeoutError, RuntimeError) as exc:
        return {**base, "prompt_id": prompt_id, "prompt_resolved": prompt_resolved,
                "error": f"wait falló: {exc}"}

    # 5. Localizar el primer PNG en los outputs (nodo SaveImage → images).
    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:
        return {**base, "prompt_id": prompt_id, "prompt_resolved": prompt_resolved,
                "error": f"el workflow no produjo imágenes (outputs={list(outputs)})"}

    # 6. Descargar la imagen a disco.
    fetched = comfyui_fetch_output_image(
        img["filename"], subfolder=img.get("subfolder", ""),
        type_=img.get("type", "output"), server=server, dest_dir=dest or ".",
    )
    if not fetched.get("ok"):
        return {**base, "prompt_id": prompt_id, "prompt_resolved": prompt_resolved,
                "error": f"fetch de imagen falló: {fetched.get('error')}"}
    image_path = fetched["path"]

    result = {**base, "ok": True, "prompt_id": prompt_id, "image_path": image_path,
              "prompt_resolved": prompt_resolved}

    if not judge:
        return result

    # 7. Juzgar el resultado con el panel multi-juez.
    judge_run_id = f"judge_{prompt_id}"
    verdict = comfyui_judge_image(image_path, prompt_resolved, server=server)
    if not verdict.get("ok"):
        # El panel falló entero (los 3 jueces caídos): la imagen ya está, no es error fatal.
        result["judge_run_id"] = judge_run_id
        result["error"] = f"juez falló (imagen generada igualmente): {verdict.get('error')}"
        return result

    result["judge"] = {"verdict": verdict["verdict"], "score": verdict["score"],
                       "votes": verdict["votes"]}
    result["judge_run_id"] = judge_run_id

    # 8. Acumular el score en la media de la skill — solo para la receta canónica
    #    (una variante con recipe_patch es una prueba; el caller decide si la promueve).
    if not recipe_patch:
        upd = comfyui_update_skill_score(slug, verdict["score"], library_dir=library_dir)
        if upd.get("ok"):
            result["score_mean"] = upd["score_mean"]
            result["score_n"] = upd["score_n"]

    return result


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


if __name__ == "__main__":
    import json

    res = comfyui_generate_with_skill_oneshot(
        "portrait_cinematic_sd15", "a woman with red hair",
        dest="/tmp/comfy_skill", seed=42, judge=True,
    )
    print(json.dumps(res, indent=2, ensure_ascii=False))
