#!/usr/bin/env python3
"""Fail when core/ imports modules outside the kernel boundary.

Allowed imports:
  - Python stdlib (sys.stdlib_module_names)
  - httpx, requests
  - core.*

Grandfathered edges shrink-only; remove entries when the import is fixed.

Usage:
    python scripts/check_kernel_boundary.py

Exit codes:
    0 — OK
    1 — boundary violated
    2 — script error
"""
from __future__ import annotations

import ast
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
CORE_DIR = REPO_ROOT / "core"

# Shrink-only: remove an entry once the import is eliminated in core/.
# Phase 0 baseline — core/config.py predates the kernel boundary and will
# shrink in Phase 3 when config moves behind a seam.
GRANDFATHERED: frozenset[tuple[str, str]] = frozenset({
    ("core/__init__.py", "persistence"),
    ("core/config.py", "persistence"),
    ("core/config.py", "tools"),
    ("core/config.py", "utils"),
})

ALLOWED_THIRD_PARTY: frozenset[str] = frozenset({"httpx", "requests"})


def _stdlib_roots() -> frozenset[str]:
    names = set(getattr(sys, "stdlib_module_names", ()))
    # Fallback for older Python if stdlib_module_names is incomplete.
    names.update(
        {
            "__future__",
            "typing",
            "typing_extensions",
            "dataclasses",
            "enum",
            "pathlib",
            "collections",
            "collections.abc",
        }
    )
    return frozenset(names)


def _module_root(name: str | None) -> str:
    if not name:
        return ""
    return name.split(".", 1)[0]


def _is_allowed(module: str | None, *, stdlib: frozenset[str]) -> bool:
    root = _module_root(module)
    if not root:
        return True
    if root in stdlib:
        return True
    if root in ALLOWED_THIRD_PARTY:
        return True
    if root == "core":
        return True
    return False


def _collect_imports(path: Path) -> set[str]:
    try:
        tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    except OSError as exc:
        print(f"ERROR: cannot read {path}: {exc}", file=sys.stderr)
        raise SystemExit(2) from exc
    except SyntaxError as exc:
        print(f"ERROR: syntax error in {path}: {exc}", file=sys.stderr)
        raise SystemExit(2) from exc

    found: set[str] = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                found.add(_module_root(alias.name))
        elif isinstance(node, ast.ImportFrom):
            if node.level and node.level > 0:
                # Relative import within core/ is always allowed.
                continue
            found.add(_module_root(node.module))
    return found


def _rel_core_path(path: Path) -> str:
    return str(path.relative_to(CORE_DIR)).replace("\\", "/")


def main() -> int:
    if not CORE_DIR.is_dir():
        print(f"ERROR: missing core directory: {CORE_DIR}", file=sys.stderr)
        return 2

    stdlib = _stdlib_roots()
    violations: list[tuple[str, str]] = []
    seen_grandfathered: set[tuple[str, str]] = set()

    for path in sorted(CORE_DIR.rglob("*.py")):
        rel = f"core/{_rel_core_path(path)}"
        for mod in sorted(_collect_imports(path)):
            if not mod or _is_allowed(mod, stdlib=stdlib):
                continue
            edge = (rel, mod)
            if edge in GRANDFATHERED:
                seen_grandfathered.add(edge)
                continue
            violations.append(edge)

    missing_grandfathered = GRANDFATHERED - seen_grandfathered
    ok = True

    if missing_grandfathered:
        ok = False
        print("ERROR: grandfathered import(s) no longer present — update GRANDFATHERED:", file=sys.stderr)
        for rel, mod in sorted(missing_grandfathered):
            print(f"  {rel} -> {mod}", file=sys.stderr)

    if violations:
        ok = False
        print("ERROR: kernel boundary violated:", file=sys.stderr)
        for rel, mod in violations:
            print(f"  {rel} imports {mod!r}", file=sys.stderr)
        print(
            "\nAllowed: stdlib, httpx, requests, core.* "
            f"(plus grandfathered {sorted(GRANDFATHERED)!r})",
            file=sys.stderr,
        )

    if ok:
        print(
            f"OK: kernel boundary clean "
            f"({len(list(CORE_DIR.rglob('*.py')))} files, "
            f"{len(GRANDFATHERED)} grandfathered)"
        )
        return 0
    return 1


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