"""Generate ``PUBLIC_API.json`` from the ``__all__`` of every public module.

The markdown ``PUBLIC_API.md`` is the human-readable contract;
``PUBLIC_API.json`` is the machine-readable companion (rule AP-10 /
DOC-08). Consumers and downstream CI scripts can ``grep`` / ``jq``
against the JSON to verify their pinned imports still exist after an
upgrade.

Why AST, not import: ``mgf.vm.interchange`` shares the ``mgf.vm``
PEP 420 namespace with the (transitional) ``mgf-vm`` umbrella dep,
which still ships a parallel copy of this subpackage. Importing the
module would resolve to whichever copy wins on ``sys.path`` — not
necessarily THIS repo's ``src/``. Reading ``__all__`` straight from
the source files via :mod:`ast` pins the contract to the code in
this repository, independent of install layout.

Schema::

    {
      "__version__": "0.1.0",
      "modules": {
        "mgf.vm.interchange": [...],
        "mgf.vm.interchange.ovf": [...],
        ...
      },
      "__generated_at_module_count": 3,
      "__generated_total_names": 14
    }

Top-level keys with leading double-underscore are metadata —
consumer parsers iterate ``modules`` for the contract list.

Usage (one-off regeneration)::

    uv run python tools/generate_public_api_json.py

Pinned by ``tests/unit/test_public_api_pin.py``: running the
generator MUST reproduce the file on disk; CI fails if they drift.
"""

from __future__ import annotations

import ast
import json
import tomllib
from pathlib import Path
from typing import Any

REPO_ROOT = Path(__file__).resolve().parent.parent
SRC_ROOT = REPO_ROOT / "src"

# Order is the import-graph order: the package facade first, then the
# modules it re-exports from. Matches the layout under src/.
PUBLIC_MODULES = (
    "mgf.vm.interchange",
    "mgf.vm.interchange.ovf",
    "mgf.vm.interchange.importer",
    "mgf.vm.interchange.exporter",
)


def _module_path(mod_name: str) -> Path:
    rel = Path(*mod_name.split("."))
    pkg_init = SRC_ROOT / rel / "__init__.py"
    if pkg_init.exists():
        return pkg_init
    return SRC_ROOT / rel.with_suffix(".py")


def _all_names(path: Path) -> list[str]:
    """Return the literal members of ``__all__`` in a source file."""
    tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    for node in tree.body:
        if not isinstance(node, ast.Assign):
            continue
        for target in node.targets:
            if isinstance(target, ast.Name) and target.id == "__all__":
                value = node.value
                if not isinstance(value, ast.List | ast.Tuple):
                    msg = f"__all__ in {path} is not a list/tuple literal"
                    raise TypeError(msg)
                names: list[str] = []
                for elt in value.elts:
                    if not isinstance(elt, ast.Constant) or not isinstance(
                        elt.value, str
                    ):
                        msg = f"__all__ in {path} has a non-string entry"
                        raise TypeError(msg)
                    names.append(elt.value)
                return sorted(names)
    return []


def _project_version() -> str:
    data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
    version = data["project"]["version"]
    if not isinstance(version, str):  # pragma: no cover - defensive
        msg = "project.version is not a string"
        raise TypeError(msg)
    return version


def build_payload() -> dict[str, Any]:
    """Build the full PUBLIC_API.json payload."""
    modules: dict[str, list[str]] = {}
    total = 0
    for mod_name in PUBLIC_MODULES:
        names = _all_names(_module_path(mod_name))
        modules[mod_name] = names
        total += len(names)
    return {
        "__version__": _project_version(),
        "modules": modules,
        "__generated_at_module_count": len(PUBLIC_MODULES),
        "__generated_total_names": total,
    }


def render_json() -> str:
    """Return the stable-formatted JSON text (sorted keys, trailing nl)."""
    return json.dumps(build_payload(), indent=2, sort_keys=True) + "\n"


def main() -> int:
    """CLI entry — regenerate PUBLIC_API.json at the repo root."""
    target = REPO_ROOT / "PUBLIC_API.json"
    target.write_text(render_json(), encoding="utf-8")
    print(f"wrote {target}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
