#!/usr/bin/env python3
"""Produce a deterministic static import-closure and forbidden-token report."""

from __future__ import annotations

import ast
import hashlib
import json
import os
import re
import stat
from collections import deque
from pathlib import Path

REPOSITORY = Path("/home/cube/projects/richard/hermes-agent")
OUTPUT = Path(
    "/home/cube/projects/richard/traning coach/.omo/evidence/task26/"
    "task26-v1-runtime-closure-static-report.redacted.json"
)
ROOTS = (
    "hermes_cli/main.py",
    "gateway/platforms/telegram.py",
    "gateway/platforms/nutrition_coaching.py",
    "gateway/platforms/nutrition_coaching_proposal.py",
    "gateway/platforms/nutrition_coaching_proposal_validation.py",
    "gateway/platforms/nutrition_coaching_judgment.py",
    "gateway/platforms/nutrition_coaching_judgment_request.py",
    "gateway/platforms/nutrition_coaching_judgment_types.py",
    "gateway/platforms/task23_portable_coach_successor.py",
)
FORBIDDEN = re.compile(
    r"(?i)(^|[^a-z0-9])trainer([^a-z0-9]|$)|"
    r"trainer_[a-z0-9_]*|[a-z0-9_]*_trainer[a-z0-9_]*|"
    r"trainer-review|trainer_review|trainer-session|trainer_session|"
    r"claim_trainer|pilot_trainer|trb[0-9]+|pt1:|트레이너|오늘PT기록|PT기록"
)


def digest(raw: bytes) -> str:
    return hashlib.sha256(raw).hexdigest()


def module_name(path: Path) -> str:
    relative = path.relative_to(REPOSITORY).with_suffix("")
    parts = list(relative.parts)
    if parts[-1] == "__init__":
        parts.pop()
    return ".".join(parts)


def module_map() -> dict[str, Path]:
    values: dict[str, Path] = {}
    for path in sorted(REPOSITORY.rglob("*.py")):
        if any(part in {".venv", "venv", "node_modules", "build"} for part in path.parts):
            continue
        values[module_name(path)] = path
    return values


def relative_module(current: str, node: ast.ImportFrom) -> str:
    if node.level == 0:
        return node.module or ""
    package = current.split(".")[:-1]
    if node.level > 1:
        package = package[: 1 - node.level]
    suffix = (node.module or "").split(".") if node.module else []
    return ".".join([*package, *suffix])


def imports(module: str, path: Path, known: dict[str, Path]) -> set[str]:
    source = path.read_text(encoding="utf-8")
    tree = ast.parse(source, filename=str(path))
    resolved: set[str] = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                if alias.name in known:
                    resolved.add(alias.name)
        elif isinstance(node, ast.ImportFrom):
            base = relative_module(module, node)
            if base in known:
                resolved.add(base)
            for alias in node.names:
                candidate = f"{base}.{alias.name}" if base else alias.name
                if candidate in known:
                    resolved.add(candidate)
        elif (
            isinstance(node, ast.Call)
            and isinstance(node.func, ast.Attribute)
            and isinstance(node.func.value, ast.Name)
            and node.func.value.id == "importlib"
            and node.func.attr == "import_module"
            and node.args
            and isinstance(node.args[0], ast.Constant)
            and isinstance(node.args[0].value, str)
            and node.args[0].value in known
        ):
            resolved.add(node.args[0].value)
    return resolved


def canonical(value: object) -> bytes:
    return json.dumps(
        value,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
        allow_nan=False,
    ).encode("utf-8")


def main() -> None:
    known = module_map()
    root_modules = tuple(module_name(REPOSITORY / root) for root in ROOTS)
    queue = deque((name, "entry_root") for name in root_modules)
    reasons: dict[str, str] = {}
    while queue:
        name, reason = queue.popleft()
        if name in reasons or name not in known:
            continue
        reasons[name] = reason
        for child in sorted(imports(name, known[name], known)):
            queue.append((child, f"static_import:{name}"))
    entries = []
    forbidden_paths = []
    total = 0
    for name in sorted(reasons, key=lambda item: str(known[item].relative_to(REPOSITORY))):
        path = known[name]
        raw = path.read_bytes()
        mode = stat.S_IMODE(path.stat(follow_symlinks=False).st_mode)
        matches = len(FORBIDDEN.findall(raw.decode("utf-8")))
        total += matches
        relative = str(path.relative_to(REPOSITORY))
        if matches:
            forbidden_paths.append({"path": relative, "match_count": matches})
        entries.append(
            {
                "path": relative,
                "sha256": digest(raw),
                "bytes": len(raw),
                "mode": f"{mode:04o}",
                "reason_in_closure": reasons[name],
            }
        )
    report = {
        "schema": "task26-static-runtime-closure-report-v1",
        "repository": str(REPOSITORY),
        "entry_roots": list(ROOTS),
        "closure_entries": entries,
        "closure_sha256": digest(canonical(entries)),
        "global_forbidden_scan": {
            "expected_match_count": 0,
            "actual_match_count": total,
            "paths": forbidden_paths,
        },
        "status": "pass" if total == 0 else "fail",
    }
    OUTPUT.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
    temporary = OUTPUT.with_suffix(".tmp")
    temporary.write_bytes(canonical(report))
    temporary.chmod(0o600)
    os.replace(temporary, OUTPUT)
    OUTPUT.chmod(0o600)


if __name__ == "__main__":
    main()
