"""comfyui_harvest_civitai_skill_oneshot — Civitai → skill candidata en una llamada.

Cosecha de Civitai una imagen con su workflow+receta embebidos y la promueve a una
skill candidata de la librería (grupo `comfyui-skill`), en un solo paso:

    search_images → fetch (segrega NSFW) → extract_recipe → save_skill (candidata)

Es la doctrina del issue 0087 aplicada a la captación de assets de internet: en vez
de reconstruir a mano una receta que ya existe en una imagen pública, se cosecha y
se guarda como candidata (`score_n=0`, `provenance.source='civitai'`) para que el
bucle de juicio/bump del grupo `comfyui-skill` la valide y la promueva si funciona.

Compone funciones del registry:

    comfyui_search_civitai_images_py_ml     (GET /api/v1/images)
    comfyui_fetch_civitai_image_py_ml        (descarga PNG, segrega NSFW)
    comfyui_extract_recipe_from_png_py_ml    (destila receta candidata)
    comfyui_save_skill_py_ml                 (persiste en la librería)

Pipeline impuro: red (HTTP) + escritura en disco.

**No baja modelos a ciegas**: si la receta cosechada referencia un checkpoint o LoRA
que NO está instalado en `<comfyui_dir>/models/`, lo reporta en `missing_models` y NO
descarga nada. Bajar esos modelos (con `comfyui_search_civitai_models` +
`comfyui_download_model`) es una decisión aparte del caller.
"""
from __future__ import annotations

import os
import sys

_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_extract_recipe_from_png import comfyui_extract_recipe_from_png
from ml.comfyui_fetch_civitai_image import comfyui_fetch_civitai_image
from ml.comfyui_save_skill import comfyui_save_skill
from ml.comfyui_search_civitai_images import comfyui_search_civitai_images


def _installed_models(comfyui_dir: str) -> set:
    """Conjunto de nombres de archivo de checkpoints + loras instalados."""
    base = os.path.expanduser(comfyui_dir)
    found = set()
    for sub in ("checkpoints", "loras"):
        d = os.path.join(base, "models", sub)
        if os.path.isdir(d):
            try:
                found.update(os.listdir(d))
            except OSError:
                pass
    return found


def _missing_models(recipe: dict, comfyui_dir: str) -> list:
    """Modelos referenciados por la receta que no están en disco (NO los descarga)."""
    installed = _installed_models(comfyui_dir)
    missing = []
    ckpt = recipe.get("checkpoint")
    if ckpt and os.path.basename(ckpt) not in installed:
        missing.append({"kind": "checkpoint", "name": ckpt})
    for lora in recipe.get("loras") or []:
        nm = lora.get("name") if isinstance(lora, dict) else None
        if nm and os.path.basename(nm) not in installed:
            missing.append({"kind": "lora", "name": nm})
    return missing


def comfyui_harvest_civitai_skill_oneshot(
    *,
    query: str | None = None,
    model_version_id: int | None = None,
    nsfw: str = "None",
    dest_dir: str,
    library_dir: str = "~/ComfyUI/skills_library",
    comfyui_dir: str = "~/ComfyUI",
    token: str | None = None,
    slug: str | None = None,
    pick_index: int = 0,
    search_limit: int = 20,
) -> dict:
    """Cosecha una imagen de Civitai y la guarda como skill candidata.

    Args:
        query: texto de búsqueda libre (best-effort, ver search_civitai_images).
            keyword-only.
        model_version_id: filtra por versión de modelo (filtro fiable). keyword-only.
        nsfw: nivel NSFW del search Civitai ("None"=SFW por defecto). keyword-only.
        dest_dir: carpeta donde se descarga el PNG (NSFW se segrega a `nsfw/`).
            keyword-only, obligatorio.
        library_dir: librería de skills. Default `~/ComfyUI/skills_library`. keyword-only.
        comfyui_dir: raíz de ComfyUI para chequear modelos instalados. keyword-only.
        token: token Civitai. None lo resuelve de `pass civitai/api-token`. keyword-only.
        slug: slug de la skill. Si None se deriva del prompt cosechado. keyword-only.
        pick_index: índice del item del search a cosechar (default 0, el primero).
            keyword-only.
        search_limit: cuántas imágenes pedir al search. keyword-only.

    Returns:
        dict {ok, slug, skill_path, png_path, recipe, nsfw, missing_models,
        image_id, has_workflow, attempts, error}. ok=False con error en cualquier
        fallo de red/guardado; 0 resultados del search → ok=False con error claro.

    Como Civitai sirve muchas imágenes recomprimidas sin workflow ComfyUI embebido
    (y su API ya no expone `meta`), el pipeline ITERA los items desde `pick_index`
    hasta encontrar el primero del que se puede destilar una receta, descartando
    (borrando de disco) los PNGs intermedios que no sirven. Prefiere los que traen
    workflow embebido.
    """
    sr = comfyui_search_civitai_images(
        query=query, model_version_id=model_version_id,
        nsfw=nsfw, limit=search_limit, token=token,
    )
    if not sr.get("ok"):
        return _err(f"search falló: {sr.get('error')}")
    items = sr.get("items") or []
    if not items:
        return _err("el search no devolvió imágenes (0 resultados)")
    if pick_index >= len(items):
        return _err(f"pick_index {pick_index} fuera de rango ({len(items)} items)")

    ctx = {"dest_dir": dest_dir, "library_dir": library_dir,
           "comfyui_dir": comfyui_dir, "token": token, "slug": slug}
    res = _harvest_from_items(items[pick_index:], ctx)
    if res.get("ok"):
        return res

    # 2º pase: si se filtró por un modelo (query resuelto / model_version_id) y
    # ninguna imagen traía workflow ComfyUI embebido, prueba el feed global
    # "Most Reactions" — donde abundan los workflows ComfyUI completos.
    used_model_filter = (model_version_id is not None) or bool(sr.get("resolved_model_version_id"))
    if used_model_filter:
        sr2 = comfyui_search_civitai_images(nsfw=nsfw, sort="Most Reactions",
                                            limit=search_limit, token=token)
        if sr2.get("ok") and sr2.get("items"):
            res2 = _harvest_from_items(sr2["items"], ctx, prev_attempts=res.get("attempts", 0))
            if res2.get("ok"):
                return res2
            res = res2
    return res


def _harvest_from_items(items: list, ctx: dict, *, prev_attempts: int = 0) -> dict:
    """Itera items: fetch → extract → save. Devuelve la primera candidata o _err."""
    attempts = prev_attempts
    last_err = "ningún item produjo una receta destilable"
    for item in items:
        attempts += 1
        img_url = item.get("url")
        is_nsfw = bool(item.get("nsfw"))
        if not img_url:
            continue
        fr = comfyui_fetch_civitai_image(img_url, dest_dir=ctx["dest_dir"],
                                         nsfw=is_nsfw, token=ctx["token"])
        if not fr.get("ok"):
            last_err = f"fetch falló: {fr.get('error')}"
            continue
        png_path = fr["path"]
        er = comfyui_extract_recipe_from_png(
            png_path, slug=ctx["slug"], civitai_meta=item.get("meta"),
            image_url=img_url, nsfw=is_nsfw,
        )
        if not er.get("ok"):
            last_err = f"extracción falló: {er.get('error')}"
            _cleanup(png_path)  # PNG sin receta útil → no dejar basura
            continue

        recipe = er["recipe"]
        missing = _missing_models(recipe, ctx["comfyui_dir"])
        sv = comfyui_save_skill(recipe, library_dir=ctx["library_dir"])
        if not sv.get("ok"):
            return _err(f"save_skill falló: {sv.get('error')}",
                        png_path=png_path, recipe=recipe, nsfw=is_nsfw,
                        missing_models=missing, image_id=item.get("id"),
                        has_workflow=er.get("has_workflow"), attempts=attempts)
        return {
            "ok": True,
            "slug": sv["slug"],
            "skill_path": sv["path"],
            "png_path": png_path,
            "recipe": recipe,
            "nsfw": is_nsfw,
            "missing_models": missing,
            "image_id": item.get("id"),
            "has_workflow": er.get("has_workflow"),
            "attempts": attempts,
            "error": "",
        }
    return _err(f"tras {attempts} intentos, {last_err}", attempts=attempts)


def _cleanup(path: str | None) -> None:
    if path and os.path.exists(path):
        try:
            os.remove(path)
        except OSError:
            pass


def _err(msg: str, **extra) -> dict:
    base = {"ok": False, "slug": "", "skill_path": "", "png_path": "", "recipe": {},
            "nsfw": False, "missing_models": [], "image_id": None,
            "has_workflow": False, "attempts": 0, "error": msg}
    base.update(extra)
    return base


if __name__ == "__main__":
    import json

    out = comfyui_harvest_civitai_skill_oneshot(
        query="cinematic portrait", nsfw="None",
        dest_dir=os.path.expanduser("~/ComfyUI/civitai_harvest"),
    )
    print(json.dumps({k: v for k, v in out.items() if k != "recipe"},
                     ensure_ascii=False, indent=2))
    if out.get("ok"):
        print("missing_models:", out["missing_models"])
