#!/usr/bin/env python3
"""PR-01 supply-chain audit: walk mgf-hrb's installed dependency tree
and fail if any UI framework is present anywhere in the transitive
closure.

import-linter (PR-01) protects the SDK's own source against
accidentally importing PySide6 / textual / click / etc. — but it
cannot stop a transitive dependency from being installed alongside.
A future bump of ``mgf-common`` could pull in a UI framework via
some shared logging / observability lib without the SDK source
visibly changing; this script catches that class of regression
before it ships.

Scope: walks the **production** dependency closure starting from
``mgf-hrb`` via :mod:`importlib.metadata`. Dev deps (`pytest`,
`import-linter`, `mgf-test-supervisor`, etc.) are NOT crawled —
they live in `[project.optional-dependencies].dev` and are not
installed by `pip install mgf-hrb`.

Exit codes:
    0 — no UI framework in the production transitive closure
    1 — at least one UI framework found (printed with dep chain)
    2 — mgf-hrb not installed in the active environment
"""

from __future__ import annotations

import importlib.metadata as md
import sys

# Mirror the PR-01 §3 forbidden-import set. Compared case-
# insensitively so e.g. `PySide6` == `pyside6` per PEP 503
# normalisation.
_FORBIDDEN_FRAMEWORKS = frozenset(
    {
        # Qt family
        "PySide6",
        "PyQt6",
        "PySide2",
        "PyQt5",
        # Qt async bridges
        "qasync",
        "asyncqt",
        # TUI
        "textual",
        "rich",
        "prompt_toolkit",
        "urwid",
        # CLI framework (argparse is stdlib — not listed)
        "click",
        "typer",
        "fire",
        # Web
        "flask",
        "fastapi",
        "django",
        "starlette",
        "aiohttp",
        # Templating
        "jinja2",
        "mako",
        "chameleon",
    },
)


def _normalise(name: str) -> str:
    return name.lower().replace("_", "-").strip()


def _direct_requires(dist_name: str) -> list[str]:
    """Return the package names this distribution directly requires
    AS PRODUCTION (i.e. without any extras).

    A requires line like ``pytest>=8.0 ; extra == 'dev'`` is a
    *dev-only* require — installing the bare distribution doesn't
    pull it in. We filter those out so we walk only the production
    closure. Without this filter, ``import-linter`` (dev-only)
    would appear in the tree and bring its OWN deps (click, rich,
    fastapi) with it, producing a false PR-01 violation.
    """
    try:
        dist = md.distribution(dist_name)
    except md.PackageNotFoundError:
        return []
    out: list[str] = []
    for req in dist.requires or []:
        # Detect env markers and skip anything gated by an extra.
        # Production = empty marker OR marker that's NOT a dev-extra.
        if ";" in req:
            head, marker = req.split(";", 1)
            if "extra ==" in marker or "extra==" in marker:
                # Dev/optional extra — not production.
                continue
        else:
            head = req
        # Drop extras-suffix (`pkg[extra]`) — the parent's own extras
        # don't propagate to our closure walk.
        head = head.split("[", 1)[0].strip()
        # Drop version spec.
        for sep in ("==", ">=", "<=", "!=", "~=", ">", "<", " "):
            if sep in head:
                head = head.split(sep, 1)[0]
        pkg = head.strip()
        if pkg:
            out.append(pkg)
    return out


def transitive_closure(root: str) -> dict[str, list[str]]:
    """Walk the closure from `root`; return {pkg: [parents]}."""
    parents: dict[str, list[str]] = {root: []}
    queue = [root]
    while queue:
        cur = queue.pop()
        for child in _direct_requires(cur):
            key = _normalise(child)
            if key not in parents:
                parents[key] = [cur]
                queue.append(key)
            else:
                if cur not in parents[key]:
                    parents[key].append(cur)
    return parents


def main() -> int:
    root = "mgf-hrb"
    try:
        md.distribution(root)
    except md.PackageNotFoundError:
        print(
            f"ERROR: {root} is not installed in this environment. Run `uv sync` first.",
            file=sys.stderr,
        )
        return 2

    closure = transitive_closure(root)
    forbidden_norm = {_normalise(f) for f in _FORBIDDEN_FRAMEWORKS}
    found = sorted(set(closure) & forbidden_norm)

    if found:
        print("FAIL — UI framework found in mgf-hrb production dep tree:")
        for pkg in found:
            chain = " -> ".join(closure[pkg])
            print(f"  {pkg}  (pulled in via: {chain})")
        print(
            "\nPR-01 (mgf-common PRESENTATION_LAYER.md) forbids any UI "
            "framework in the SDK's transitive dep closure. If this dep "
            "is genuinely required, move it to "
            "[project.optional-dependencies].dev or hrb-app — never the "
            "SDK's production deps.",
            file=sys.stderr,
        )
        return 1

    print(f"OK — no UI framework in {len(closure)} transitive deps of {root}.")
    return 0


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