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

Adapted from ``mgf-common/tools/generate_public_api_json.py`` for this
sibling. ``PUBLIC_API.md`` is the human-readable contract; this JSON is
the machine-readable companion (DOC-08 / AP-10) — consumers ``jq`` it to
verify their pinned imports still exist after an upgrade.

Schema::

    {
      "__version__": "0.1.0",
      "modules": {
        "mgf.vm.forensics": ["Case", "CaseStore", ...],
        ...
      },
      "__generated_at_module_count": N,
      "__generated_total_names": M
    }

Top-level keys with a leading double-underscore are metadata; consumer
parsers iterate ``modules``.

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 byte-for-byte; CI fails on drift.

Why static (AST) extraction
---------------------------
This repo and the transitional ``mgf-vm`` umbrella both contribute to
the ``mgf.vm`` namespace. The umbrella ships ``mgf/vm/__init__.py`` as a
regular package, which collapses the PEP-420 namespace: while the
umbrella is installed editable, ``import mgf.vm.forensics`` resolves to
the umbrella's copy, NOT this repo's ``src/``. To describe THIS repo's
shipped surface deterministically (regardless of install state) we read
``__all__`` straight from the source files via ``ast`` — no import.
This dependency flips out when ``mgf-vm-core`` lands (Phase 0 Stream A)
and the umbrella stops shipping ``mgf.vm.forensics``.
"""

from __future__ import annotations

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

_REPO_ROOT = Path(__file__).resolve().parent.parent
_PKG_ROOT = _REPO_ROOT / "src" / "mgf" / "vm" / "forensics"
_OUTPUT = _REPO_ROOT / "PUBLIC_API.json"

# The public modules of this library, in a deterministic order. The
# top-level package re-exports a curated subset; the analysis / capture
# / reports subpackages and the standalone modules carry their own
# ``__all__``. PUBLIC_API documents each importable surface that ships
# an ``__all__``.
PUBLIC_MODULES: tuple[tuple[str, str], ...] = (
    ("mgf.vm.forensics", "__init__.py"),
    ("mgf.vm.forensics.case", "case.py"),
    ("mgf.vm.forensics.store", "store.py"),
    ("mgf.vm.forensics.manifest", "manifest.py"),
    ("mgf.vm.forensics.archive", "archive.py"),
    ("mgf.vm.forensics.render", "render.py"),
    ("mgf.vm.forensics.live", "live.py"),
    ("mgf.vm.forensics.analysis", "analysis/__init__.py"),
    ("mgf.vm.forensics.capture", "capture/__init__.py"),
    ("mgf.vm.forensics.reports", "reports/__init__.py"),
)


def _extract_all(source_path: Path) -> list[str]:
    """Return the ``__all__`` list literal from a module, statically."""
    tree = ast.parse(source_path.read_text(encoding="utf-8"))
    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__":
                if not isinstance(node.value, ast.List | ast.Tuple):
                    msg = f"__all__ in {source_path} is not a list/tuple literal"
                    raise ValueError(msg)
                names: list[str] = []
                for elt in node.value.elts:
                    if not isinstance(elt, ast.Constant) or not isinstance(elt.value, str):
                        msg = f"__all__ in {source_path} has a non-string element"
                        raise ValueError(msg)
                    names.append(elt.value)
                return names
    msg = f"no __all__ found in {source_path}"
    raise ValueError(msg)


def _version() -> str:
    names = ast.parse((_PKG_ROOT / "__init__.py").read_text(encoding="utf-8"))
    for node in names.body:
        if isinstance(node, ast.Assign):
            for target in node.targets:
                if isinstance(target, ast.Name) and target.id == "__version__":
                    value = node.value
                    if isinstance(value, ast.Constant) and isinstance(value.value, str):
                        return value.value
    msg = "no __version__ found in mgf/vm/forensics/__init__.py"
    raise ValueError(msg)


def build_payload() -> dict[str, Any]:
    modules: dict[str, list[str]] = {}
    total = 0
    for mod_name, rel in PUBLIC_MODULES:
        names = sorted(_extract_all(_PKG_ROOT / rel))
        modules[mod_name] = names
        total += len(names)
    return {
        "__version__": _version(),
        "modules": modules,
        "__generated_at_module_count": len(modules),
        "__generated_total_names": total,
    }


def render(payload: dict[str, Any]) -> str:
    return json.dumps(payload, indent=2, ensure_ascii=False) + "\n"


def main() -> int:
    payload = build_payload()
    _OUTPUT.write_text(render(payload), encoding="utf-8")
    sys.stdout.write(
        f"wrote {_OUTPUT.name}: {payload['__generated_at_module_count']} modules, "
        f"{payload['__generated_total_names']} names\n",
    )
    return 0


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