#!/usr/bin/env python3
"""Build a machine-readable catalog of the entire curriculum.

Requires Python 3.10+ (PEP 604 union types, Path.is_relative_to).

Walks every `phases/NN-slug/MM-slug/` lesson directory on disk and emits a
single JSON document with the truth of what exists in the repo: phases,
lessons, code files, outputs (skills / prompts / agents), and totals.

Usage:
    python3 scripts/build_catalog.py                     # write catalog.json at repo root
    python3 scripts/build_catalog.py --out path/to/catalog.json
    python3 scripts/build_catalog.py --stdout            # write to stdout, do not touch repo

Output shape (schema_version 1):
    {
      "schema_version": 1,
      "totals": {"phases": ..., "lessons": ..., "skills": ..., "prompts": ..., "agents": ..., "code_files": ...},
      "phases": [
        {
          "num": 0,
          "slug": "00-setup-and-tooling",
          "title": "Setup and Tooling",
          "lesson_count": 12,
          "lessons": [
            {
              "num": 1,
              "slug": "01-...",
              "title": "...",                  # H1 from docs/en.md
              "path": "phases/00-.../01-...",
              "has_docs": true,
              "has_code": true,
              "has_quiz": false,
              "has_notebook": false,
              "code_files": ["main.py", ...],
              "outputs": [
                {"type": "skill", "name": "...", "path": "...", "version": "1.0.0", "description": "...", "tags": [...]}
              ]
            }
          ]
        }
      ]
    }

Stdlib only. No dependencies. Reuses frontmatter parsing logic similar to
install_skills.py but inlined to keep the script self-contained.
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path
from typing import Iterable

sys.path.insert(0, str(Path(__file__).resolve().parent))
from _lib import parse_frontmatter as _parse_frontmatter  # noqa: E402

ROOT = Path(__file__).resolve().parent.parent
PHASES_DIR = ROOT / "phases"

PHASE_DIR_RE = re.compile(r"^([0-9]{2})-([a-z0-9][a-z0-9-]*)$")
LESSON_DIR_RE = re.compile(r"^([0-9]{2})-([a-z0-9][a-z0-9-]*)$")
H1_RE = re.compile(r"^#\s+(.+?)\s*$", re.MULTILINE)
ARTIFACT_TYPES = ("skill", "prompt", "agent")
CODE_SUFFIXES = {".py", ".ts", ".tsx", ".js", ".mjs", ".rs", ".jl", ".go", ".swift", ".ipynb"}


def slug_to_title(slug: str) -> str:
    words = slug.split("-")
    fixups = {
        "ai": "AI",
        "ml": "ML",
        "llm": "LLM",
        "llms": "LLMs",
        "nlp": "NLP",
        "rl": "RL",
        "mcp": "MCP",
        "rag": "RAG",
        "api": "API",
        "rlhf": "RLHF",
        "dpo": "DPO",
        "lora": "LoRA",
        "cnn": "CNN",
        "rnn": "RNN",
        "rnns": "RNNs",
        "cnns": "CNNs",
        "gpt": "GPT",
        "tfidf": "TF-IDF",
        "pos": "POS",
        "ner": "NER",
        "asr": "ASR",
        "tts": "TTS",
        "ios": "iOS",
        "lats": "LATS",
        "rewoo": "ReWoo",
        "htn": "HTN",
        "sft": "SFT",
    }
    return " ".join(fixups.get(w, w.capitalize()) for w in words)


def parse_frontmatter(text: str) -> dict[str, object]:
    return _parse_frontmatter(text) or {}


def read_h1(doc_path: Path) -> str | None:
    try:
        text = doc_path.read_text(encoding="utf-8")
    except (FileNotFoundError, UnicodeDecodeError):
        return None
    match = H1_RE.search(text)
    return match.group(1).strip() if match else None


def list_code_files(code_dir: Path) -> list[str]:
    if not code_dir.is_dir():
        return []
    files = []
    for path in sorted(code_dir.rglob("*")):
        if path.is_file() and path.suffix in CODE_SUFFIXES:
            files.append(path.relative_to(code_dir).as_posix())
    return files


def parse_artifact(path: Path) -> dict[str, object] | None:
    stem = path.stem
    artifact_type: str | None = None
    for t in ARTIFACT_TYPES:
        if stem.startswith(f"{t}-"):
            artifact_type = t
            break
    if artifact_type is None:
        return None
    try:
        text = path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        return None
    meta = parse_frontmatter(text)
    name = str(meta.get("name", "")).strip() or stem
    tags = meta.get("tags", [])
    if not isinstance(tags, list):
        tags = []
    return {
        "type": artifact_type,
        "name": name,
        "path": path.relative_to(ROOT).as_posix(),
        "version": str(meta.get("version", "")).strip(),
        "description": str(meta.get("description", "")).strip(),
        "tags": list(tags),
    }


def list_outputs(outputs_dir: Path) -> list[dict[str, object]]:
    if not outputs_dir.is_dir():
        return []
    artifacts: list[dict[str, object]] = []
    for path in sorted(outputs_dir.iterdir()):
        if path.suffix != ".md" or not path.is_file():
            continue
        record = parse_artifact(path)
        if record is not None:
            artifacts.append(record)
    return artifacts


def build_lesson_entry(lesson_dir: Path) -> dict[str, object] | None:
    match = LESSON_DIR_RE.match(lesson_dir.name)
    if not match:
        return None
    num = int(match.group(1))
    slug = match.group(2)
    docs_path = lesson_dir / "docs" / "en.md"
    code_dir = lesson_dir / "code"
    outputs_dir = lesson_dir / "outputs"
    notebook_dir = lesson_dir / "notebook"
    quiz_path = lesson_dir / "quiz.json"
    code_files = list_code_files(code_dir)
    outputs = list_outputs(outputs_dir)
    has_docs = docs_path.is_file()
    if not has_docs and not code_files and not outputs and not quiz_path.is_file():
        return None
    title = read_h1(docs_path) or slug_to_title(slug)
    return {
        "num": num,
        "slug": lesson_dir.name,
        "title": title,
        "path": lesson_dir.relative_to(ROOT).as_posix(),
        "has_docs": has_docs,
        "has_code": code_dir.is_dir(),
        "has_quiz": quiz_path.is_file(),
        "has_notebook": notebook_dir.is_dir(),
        "code_files": code_files,
        "outputs": outputs,
    }


def iter_phase_dirs() -> Iterable[Path]:
    if not PHASES_DIR.is_dir():
        return
    for path in sorted(PHASES_DIR.iterdir()):
        if path.is_dir() and PHASE_DIR_RE.match(path.name):
            yield path


def build_phase_entry(phase_dir: Path) -> dict[str, object]:
    match = PHASE_DIR_RE.match(phase_dir.name)
    assert match is not None
    num = int(match.group(1))
    slug = match.group(2)
    lessons: list[dict[str, object]] = []
    for lesson_dir in sorted(phase_dir.iterdir()):
        if lesson_dir.is_dir():
            entry = build_lesson_entry(lesson_dir)
            if entry is not None:
                lessons.append(entry)
    return {
        "num": num,
        "slug": phase_dir.name,
        "title": slug_to_title(slug),
        "lesson_count": len(lessons),
        "lessons": lessons,
    }


def compute_totals(phases: list[dict[str, object]]) -> dict[str, int]:
    totals = {
        "phases": len(phases),
        "lessons": 0,
        "skills": 0,
        "prompts": 0,
        "agents": 0,
        "code_files": 0,
    }
    for phase in phases:
        for lesson in phase["lessons"]:
            totals["lessons"] += 1
            totals["code_files"] += len(lesson["code_files"])
            for artifact in lesson["outputs"]:
                key = f"{artifact['type']}s"
                totals[key] = totals.get(key, 0) + 1
    return totals


def build_catalog() -> dict[str, object]:
    phases = [build_phase_entry(p) for p in iter_phase_dirs()]
    catalog = {
        "schema_version": 1,
        "totals": compute_totals(phases),
        "phases": phases,
    }
    return catalog


def main(argv: list[str]) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--out",
        type=Path,
        default=ROOT / "catalog.json",
        help="output path (default: <repo>/catalog.json)",
    )
    parser.add_argument(
        "--stdout",
        action="store_true",
        help="write JSON to stdout instead of a file",
    )
    args = parser.parse_args(argv)

    catalog = build_catalog()
    payload = json.dumps(catalog, indent=2, ensure_ascii=False) + "\n"

    if args.stdout:
        sys.stdout.write(payload)
        return 0

    args.out.parent.mkdir(parents=True, exist_ok=True)
    args.out.write_text(payload, encoding="utf-8")
    totals = catalog["totals"]
    sys.stdout.write(
        f"catalog: {args.out.relative_to(ROOT) if args.out.is_relative_to(ROOT) else args.out}\n"
    )
    sys.stdout.write(
        f"  phases={totals['phases']} lessons={totals['lessons']} "
        f"skills={totals['skills']} prompts={totals['prompts']} "
        f"agents={totals['agents']} code_files={totals['code_files']}\n"
    )
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
