"""comfyui_save_skill — persiste una receta de *skill* ComfyUI en la libreria de disco.

Una **skill** es una receta versionada del grupo `comfyui-skill`. Esta funcion valida el schema
minimo de la receta y la escribe en la libreria:

    <library_dir>/<slug>/
        recipe.json          # version actual (la receta tal cual, sin mutar)
        versions/vN.json      # snapshot inmutable de cada save (N incremental)
        exports/              # plantillas de workflow exportadas (vacio al crear)
        samples/              # imagenes de muestra (vacio al crear)
        growth_log.jsonl      # bitacora append-only de cada save
    <library_dir>/INDEX.md    # indice regenerado con todas las skills

`library_dir` por defecto `~/ComfyUI/skills_library`. La funcion NO muta la receta: lo que se
escribe en `recipe.json` es identico al dict de entrada (garantiza round-trip con
`comfyui_load_skill`).

Impura: escribe archivos en disco.
"""

import json
import os
import time

DEFAULT_LIBRARY = "~/ComfyUI/skills_library"

_REQUIRED = ("slug", "base_workflow", "version")


def _lib_dir(library_dir):
    return os.path.expanduser(library_dir or DEFAULT_LIBRARY)


def _validate_recipe(recipe):
    """Devuelve una lista de errores de schema (vacia si la receta es valida)."""
    errors = []
    if not isinstance(recipe, dict):
        return [f"recipe debe ser dict, no {type(recipe).__name__}"]
    for key in _REQUIRED:
        val = recipe.get(key)
        if not isinstance(val, str) or not val:
            errors.append(f"campo requerido ausente o vacio: {key!r}")
    slug = recipe.get("slug", "")
    if isinstance(slug, str) and slug and ("/" in slug or "\\" in slug or slug.startswith(".")):
        errors.append(f"slug invalido (no puede contener separadores de ruta ni empezar por '.'): {slug!r}")
    if "prompt_scaffold" in recipe and not isinstance(recipe["prompt_scaffold"], dict):
        errors.append("prompt_scaffold debe ser dict")
    if "params" in recipe and not isinstance(recipe["params"], dict):
        errors.append("params debe ser dict")
    if "loras" in recipe and not isinstance(recipe["loras"], list):
        errors.append("loras debe ser lista")
    if "blocks" in recipe and not isinstance(recipe["blocks"], list):
        errors.append("blocks debe ser lista")
    return errors


def _write_json(path, obj):
    with open(path, "w", encoding="utf-8") as fh:
        json.dump(obj, fh, indent=2, ensure_ascii=False)


def _rewrite_index(lib):
    """Regenera <library_dir>/INDEX.md listando todas las skills (best-effort)."""
    rows = []
    for slug in sorted(os.listdir(lib)):
        recipe_path = os.path.join(lib, slug, "recipe.json")
        if not os.path.isfile(recipe_path):
            continue
        try:
            with open(recipe_path, encoding="utf-8") as fh:
                r = json.load(fh)
        except (OSError, json.JSONDecodeError):
            continue
        prov = r.get("provenance") or {}
        nsfw = "yes" if prov.get("nsfw") else "no"
        rows.append(
            f"| {slug} | {r.get('title', '')} | {r.get('base_workflow', '')} | "
            f"{r.get('version', '')} | {r.get('score_mean', 0)} | {nsfw} |"
        )
    lines = [
        "# Skills library — ComfyUI",
        "",
        "Recetas versionadas del grupo `comfyui-skill`. Una fila por skill.",
        "",
        "| slug | title | base_workflow | version | score_mean | nsfw |",
        "|---|---|---|---|---|---|",
        *rows,
        "",
    ]
    with open(os.path.join(lib, "INDEX.md"), "w", encoding="utf-8") as fh:
        fh.write("\n".join(lines))


def comfyui_save_skill(recipe: dict, library_dir: str = None) -> dict:
    """Valida y persiste una receta de skill en la libreria de disco.

    Args:
        recipe: dict de la receta (schema `comfyui-skill`). Requiere al menos `slug`,
            `base_workflow` y `version` (strings no vacios). No se muta.
        library_dir: raiz de la libreria. Default `~/ComfyUI/skills_library`.

    Returns:
        dict ``{ok, slug, path, recipe_path, version_file, n_versions, error}``. En error de
        validacion o de escritura, ``ok=False`` y ``error`` describe la causa; nunca lanza.
    """
    errors = _validate_recipe(recipe)
    if errors:
        return {"ok": False, "slug": recipe.get("slug", "") if isinstance(recipe, dict) else "",
                "path": "", "recipe_path": "", "version_file": "", "n_versions": 0,
                "error": "receta invalida: " + "; ".join(errors)}

    slug = recipe["slug"]
    lib = _lib_dir(library_dir)
    skill_dir = os.path.join(lib, slug)
    versions_dir = os.path.join(skill_dir, "versions")
    try:
        for sub in ("versions", "exports", "samples"):
            os.makedirs(os.path.join(skill_dir, sub), exist_ok=True)

        existing = [f for f in os.listdir(versions_dir)
                    if f.startswith("v") and f.endswith(".json")]
        n = len(existing) + 1
        version_file = os.path.join(versions_dir, f"v{n}.json")
        recipe_path = os.path.join(skill_dir, "recipe.json")

        _write_json(recipe_path, recipe)
        _write_json(version_file, recipe)

        # bitacora append-only (best-effort, no rompe el save si falla)
        try:
            entry = {"ts": int(time.time()), "snapshot": f"v{n}",
                     "recipe_version": recipe.get("version", ""), "action": "save"}
            with open(os.path.join(skill_dir, "growth_log.jsonl"), "a", encoding="utf-8") as fh:
                fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
        except OSError:
            pass

        try:
            _rewrite_index(lib)
        except OSError:
            pass

        return {"ok": True, "slug": slug, "path": skill_dir, "recipe_path": recipe_path,
                "version_file": version_file, "n_versions": n, "error": ""}
    except OSError as exc:
        return {"ok": False, "slug": slug, "path": skill_dir, "recipe_path": "",
                "version_file": "", "n_versions": 0, "error": f"fallo de escritura: {exc}"}


if __name__ == "__main__":
    demo = {
        "schema_version": 1, "slug": "demo_skill", "version": "1.0.0",
        "title": "Demo", "base_workflow": "txt2img",
        "checkpoint": "IMG_dreamshaper_8.safetensors",
        "params": {"steps": 20, "cfg": 7.0},
        "prompt_scaffold": {"positive": "{subject}", "negative": "", "trigger_words": []},
        "provenance": {"source": "manual", "nsfw": False},
    }
    res = comfyui_save_skill(demo, library_dir="/tmp/skills_demo")
    print(json.dumps(res, indent=2, ensure_ascii=False))
