#!/usr/bin/env python3
"""tag_unused_pending — etiqueta cada funcion del registry sin consumidor con
el tag `pendiente-usar`.

Issue 0062: politica explicita es marcar (no borrar) las funciones unused
para que sean visibles como "disponibles, esperando consumidor".

Flujo:
  1. Invoca `./fn doctor unused --json` para obtener IDs.
  2. Lee `registry.db.functions.file_path` para localizar el .md de cada uno.
  3. Anade `pendiente-usar` al campo `tags` del frontmatter YAML. Idempotente.
  4. Tras correr el pipeline ejecutar `./fn index` para que registry.db quede
     coherente.

Re-ejecutable sin efectos secundarios. Para quitar el tag de una funcion
(porque vuelve a usarse): borrar manualmente la entrada del frontmatter y
re-indexar.

Limpieza del tag obsoleto: si una funcion ya no esta en la lista de unused
pero todavia tiene el tag, este script tambien lo retira (mantener
coherencia entre estado real y etiqueta).
"""

from __future__ import annotations

import json
import re
import sqlite3
import subprocess
import sys
from pathlib import Path

TAG = "pendiente-usar"


def find_root() -> Path:
    here = Path(__file__).resolve()
    for parent in (here, *here.parents):
        if (parent / "registry.db").exists():
            return parent
    sys.exit("registry.db not found")


def get_unused_ids(root: Path) -> list[str]:
    res = subprocess.run(
        [str(root / "fn"), "doctor", "unused", "--json"],
        capture_output=True, text=True, check=True, cwd=root,
    )
    data = json.loads(res.stdout) or []
    return [e["ID"] for e in data]


def load_id_to_md(root: Path) -> dict[str, Path]:
    conn = sqlite3.connect(str(root / "registry.db"))
    rows = conn.execute("SELECT id, name, file_path FROM functions").fetchall()
    conn.close()
    out: dict[str, Path] = {}
    for fid, name, fp in rows:
        if not fp or not name:
            continue
        # Try same dir as the .py with <name>.md
        py_path = root / fp
        candidate = py_path.parent / f"{name}.md"
        if candidate.exists():
            out[fid] = candidate
            continue
        # Fallback: derive .md from .py path with same stem (for Go single-fn files)
        derived = root / Path(fp).with_suffix(".md")
        if derived.exists():
            out[fid] = derived
    return out


_FM_RE = re.compile(r"^---\n(.*?)\n---\n(.*)$", re.DOTALL)
_INLINE_RE = re.compile(r"^tags:\s*\[(.*?)\]\s*$", re.MULTILINE)
_BLOCK_RE = re.compile(r"^tags:\s*\n((?:\s+-\s+\S.*\n)+)", re.MULTILINE)


def modify_tag(text: str, tag: str, add: bool) -> tuple[str, bool]:
    """Return (new_text, changed). If add=True, ensure tag present. If add=False, remove it."""
    m = _FM_RE.match(text)
    if not m:
        return text, False
    fm, body = m.group(1), m.group(2)

    inline_m = _INLINE_RE.search(fm)
    if inline_m:
        raw = inline_m.group(1).strip()
        items = [x.strip().strip("'\"") for x in raw.split(",")] if raw else []
        items = [x for x in items if x]
        has = tag in items
        if add and not has:
            items.append(tag)
        elif not add and has:
            items = [x for x in items if x != tag]
        else:
            return text, False
        new_line = "tags: [" + ", ".join(items) + "]"
        new_fm = _INLINE_RE.sub(new_line, fm, count=1)
        return f"---\n{new_fm}\n---\n{body}", True

    block_m = _BLOCK_RE.search(fm)
    if block_m:
        items_block = block_m.group(1)
        item_re = re.compile(rf"^(\s+-\s+){re.escape(tag)}\s*$", re.MULTILINE)
        has = bool(item_re.search(items_block))
        if add and not has:
            indent_m = re.search(r"^(\s+-\s+)\S", items_block, re.MULTILINE)
            prefix = indent_m.group(1)
            new_block = items_block + f"{prefix}{tag}\n"
        elif not add and has:
            new_block = item_re.sub("", items_block)
            # collapse double newlines
            new_block = re.sub(r"\n{2,}", "\n", new_block)
            if not new_block.strip():
                # All items removed: replace whole block with inline []
                new_fm = (fm[:block_m.start()] + "tags: []\n" + fm[block_m.end():]).rstrip()
                return f"---\n{new_fm}\n---\n{body}", True
        else:
            return text, False
        new_fm = fm[:block_m.start()] + "tags:\n" + new_block + fm[block_m.end():]
        return f"---\n{new_fm}\n---\n{body}", True

    # No tags line. Append inline tags: [tag] if adding; nothing to remove.
    if add:
        return f"---\n{fm}\ntags: [{tag}]\n---\n{body}", True
    return text, False


def main() -> None:
    root = find_root()
    print(f"root: {root}", file=sys.stderr)

    unused_ids = set(get_unused_ids(root))
    print(f"unused (from fn doctor): {len(unused_ids)}", file=sys.stderr)

    id_to_md = load_id_to_md(root)
    print(f"functions with .md on disk: {len(id_to_md)}", file=sys.stderr)

    added = 0
    removed = 0
    skipped_no_change = 0
    missing_md = 0

    for fid, md in id_to_md.items():
        text = md.read_text(encoding="utf-8")
        if fid in unused_ids:
            new_text, changed = modify_tag(text, TAG, add=True)
            if changed:
                md.write_text(new_text, encoding="utf-8")
                added += 1
            else:
                skipped_no_change += 1
        else:
            # not unused anymore — strip tag if present
            new_text, changed = modify_tag(text, TAG, add=False)
            if changed:
                md.write_text(new_text, encoding="utf-8")
                removed += 1

    for fid in unused_ids - set(id_to_md):
        missing_md += 1

    print(
        f"summary: added={added} removed={removed} "
        f"already_tagged={skipped_no_change} missing_md={missing_md}",
    )
    print("run `./fn index` next to refresh registry.db.")


if __name__ == "__main__":
    main()
