"""Generate <app>_modules_generated.cpp from app.md uses_modules + modules/*/module.md.

Stand-alone — no dependencies beyond PyYAML. Invoked from CMake at configure time.
Reads YAML frontmatter directly (no registry.db dependency, no Go binary).
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path
from typing import Optional

import yaml


def _read_frontmatter(md_path: Path) -> dict:
    if not md_path.exists():
        return {}
    text = md_path.read_text(encoding="utf-8")
    if not text.startswith("---\n") and not text.startswith("---\r\n"):
        return {}
    end = text.find("\n---", 4)
    if end < 0:
        return {}
    raw = text[4:end]
    try:
        return yaml.safe_load(raw) or {}
    except yaml.YAMLError:
        return {}


def _escape_c_string(s: str) -> str:
    out = []
    for ch in s or "":
        if ch == "\\":
            out.append("\\\\")
        elif ch == '"':
            out.append('\\"')
        elif ch == "\n":
            out.append("\\n")
        elif ch == "\r":
            out.append("\\r")
        elif ch == "\t":
            out.append("\\t")
        elif ord(ch) < 32:
            out.append(f"\\x{ord(ch):02x}")
        else:
            out.append(ch)
    return "".join(out)


def _resolve_module(modules_root: Path, mod_id: str) -> Optional[dict]:
    """mod_id is e.g. `data_table_cpp`. Lookup module.md by name (strip _<lang>)."""
    name = mod_id
    for suffix in ("_cpp", "_py", "_ts", "_bash", "_go"):
        if name.endswith(suffix):
            name = name[: -len(suffix)]
            break
    md = modules_root / name / "module.md"
    fm = _read_frontmatter(md)
    if not fm:
        return None
    return {
        "name": fm.get("name", name),
        "version": fm.get("version", "0.0.0"),
        "description": fm.get("description", ""),
    }


def generate(app_md: Path, modules_root: Path, app_name: str, out_path: Path) -> int:
    fm = _read_frontmatter(app_md)
    uses_modules = fm.get("uses_modules") or []
    if not isinstance(uses_modules, list):
        uses_modules = []

    # Toda app C++ procesada por add_imgui_app enlaza fn_framework. Inyectamos
    # framework_cpp implicito si la app es C++ y no lo declara explicitamente.
    # Asi el array embebido refleja la version real del framework linkeado,
    # sin pedir a cada app.md que lo declare a mano.
    lang = str(fm.get("lang", "")).lower()
    if lang == "cpp":
        already = [str(m) for m in uses_modules]
        if not any(m.startswith("framework_") for m in already):
            uses_modules = ["framework_cpp"] + already

    entries: list[dict] = []
    missing: list[str] = []
    for mid in uses_modules:
        info = _resolve_module(modules_root, str(mid))
        if info is None:
            missing.append(str(mid))
            continue
        entries.append(info)

    lines: list[str] = []
    lines.append(f"// Auto-generated by codegen_app_modules.py — do not edit.")
    lines.append(f"// App: {app_name}")
    lines.append(f"// Source of truth: {app_md.as_posix()} (uses_modules)")
    lines.append("")
    lines.append('#include "app_modules.h"')
    lines.append("")
    lines.append("namespace fn {")
    if entries:
        lines.append("const ModuleInfo app_modules_array[] = {")
        for e in entries:
            lines.append(
                '    { "%s", "%s", "%s" },'
                % (
                    _escape_c_string(e["name"]),
                    _escape_c_string(e["version"]),
                    _escape_c_string(e["description"]),
                )
            )
        lines.append("};")
        lines.append(f"const unsigned long app_modules_count = {len(entries)};")
    else:
        lines.append("const ModuleInfo app_modules_array[1] = { { nullptr, nullptr, nullptr } };")
        lines.append("const unsigned long app_modules_count = 0;")

    # Header badge identity — auto-derivado del bloque `icon:` del app.md.
    # Permite que el framework muestre el badge accent en viewports secundarios
    # sin tocar main.cpp. Coherente con App Hub (mismo hex que la tarjeta).
    icon_block = fm.get("icon") or {}
    accent_hex = str(icon_block.get("accent", "") or "")
    glyph_name = str(icon_block.get("phosphor", "") or "")
    lines.append(
        f'const char* const app_header_accent_hex = "{_escape_c_string(accent_hex)}";'
    )
    lines.append(
        f'const char* const app_header_glyph_name = "{_escape_c_string(glyph_name)}";'
    )

    lines.append("} // namespace fn")
    lines.append("")

    new_content = "\n".join(lines)

    # Idempotent: skip rewrite when content matches.
    if out_path.exists() and out_path.read_text(encoding="utf-8") == new_content:
        return 0 if not missing else 2

    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(new_content, encoding="utf-8")

    if missing:
        sys.stderr.write(
            f"codegen_app_modules: WARNING — module(s) not found: {', '.join(missing)} "
            f"(app {app_name})\n"
        )
        return 2
    return 0


def main() -> int:
    ap = argparse.ArgumentParser(description="Generate <app>_modules_generated.cpp from app.md")
    ap.add_argument("--app-md", required=True, help="Path to app.md")
    ap.add_argument("--modules-root", required=True, help="Path to modules/ root")
    ap.add_argument("--app-name", required=True, help="App name (for comment header)")
    ap.add_argument("--out", required=True, help="Output path for generated .cpp")
    args = ap.parse_args()

    rc = generate(
        app_md=Path(args.app_md),
        modules_root=Path(args.modules_root),
        app_name=args.app_name,
        out_path=Path(args.out),
    )
    return 0 if rc in (0, 2) else rc


if __name__ == "__main__":
    sys.exit(main())
