"""Reviewer session boundary tracking for workflow-load gate (#403).

Pre-review commands executed before ``gitea_load_review_workflow`` must be
classified. Boundary violations block downstream reviewer mutations even when
workflow hash proof is present.
"""

from __future__ import annotations

import os
import re
from typing import Any

CLASSIFICATION_READ_ONLY_INVENTORY = "read_only_inventory"
CLASSIFICATION_DIAGNOSTIC = "diagnostic"
CLASSIFICATION_BOUNDARY_VIOLATION = "boundary_violation"
CLASSIFICATION_UNCLASSIFIED = "unclassified"

ALLOWED_CLASSIFICATIONS = frozenset({
    CLASSIFICATION_READ_ONLY_INVENTORY,
    CLASSIFICATION_DIAGNOSTIC,
    CLASSIFICATION_BOUNDARY_VIOLATION,
    CLASSIFICATION_UNCLASSIFIED,
})

_PRE_REVIEW_COMMANDS: list[dict[str, Any]] = []

_READ_ONLY_INVENTORY_PATTERNS = (
    re.compile(
        r"\bgitea[_-](?:list|view|whoami|get[-_]|resolve[-_]task|check[-_]pr|route[-_]task)",
        re.I,
    ),
    re.compile(r"\bgit\s+(?:fetch|remote\s+update|branch\s+-a|log|show|rev-parse)\b", re.I),
    re.compile(r"\bgit\s+status\b", re.I),
    re.compile(r"\bgit\s+worktree\s+list\b", re.I),
)

_DIAGNOSTIC_PATTERNS = (
    re.compile(r"\bgit\s+diff(?:\s+--stat)?\b", re.I),
    re.compile(r"\bwhich\s+pytest\b", re.I),
    re.compile(r"\bpytest\s+--version\b", re.I),
)

_BOUNDARY_VIOLATION_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
    (re.compile(r"\b(?:pytest|python\s+-m\s+pytest|python\s+-m\s+unittest)\b", re.I),
     "validation command before workflow load"),
    (re.compile(r"\bprofiles\.json\b", re.I), "local profile config inspection"),
    (re.compile(r"\bgitea-mcp(?:\.v2-contexts)?\.json\b", re.I),
     "local Gitea MCP config inspection"),
    (re.compile(r"\b\.env(?:\.|$|\b)", re.I), "credential file inspection"),
    (re.compile(r"\bkeychain\b", re.I), "credential store inspection"),
    (re.compile(r"\bpkill\b", re.I), "MCP repair activity"),
    (re.compile(r"\b(?:edit|write|modify).{0,40}\bmcp\b", re.I),
     "MCP config exploration"),
    (re.compile(r"\bgit\s+(?:add|commit|reset|clean|checkout|merge|rebase|push)\b", re.I),
     "git mutation before workflow load"),
)


def clear_pre_review_commands() -> None:
    """Test helper and session reset."""
    global _PRE_REVIEW_COMMANDS
    _PRE_REVIEW_COMMANDS = []


def pre_review_commands() -> list[dict[str, Any]]:
    """Return a shallow copy of recorded pre-review commands."""
    return [dict(entry) for entry in _PRE_REVIEW_COMMANDS]


def _normalize_path(path: str | None) -> str:
    return os.path.realpath(os.path.abspath((path or "").strip() or os.getcwd()))


def is_main_checkout_path(cwd: str | None, project_root: str | None) -> bool:
    """True when *cwd* is the stable control checkout (not under branches/)."""
    if not project_root:
        return False
    root = _normalize_path(project_root)
    path = _normalize_path(cwd)
    if path != root:
        return False
    marker = f"{os.sep}branches{os.sep}"
    return marker not in path


def classify_pre_review_command(
    command: str,
    *,
    cwd: str | None = None,
    project_root: str | None = None,
) -> dict[str, Any]:
    """Classify a command executed before workflow load."""
    text = (command or "").strip()
    path = _normalize_path(cwd)
    root = _normalize_path(project_root) if project_root else None
    reasons: list[str] = []

    for pattern, label in _BOUNDARY_VIOLATION_PATTERNS:
        if pattern.search(text):
            if label.startswith("validation") and root and not is_main_checkout_path(path, root):
                continue
            if label.startswith("git mutation") and root and not is_main_checkout_path(path, root):
                continue
            reasons.append(label)
            return {
                "command": text,
                "cwd": path,
                "classification": CLASSIFICATION_BOUNDARY_VIOLATION,
                "reasons": reasons,
            }

    for pattern in _READ_ONLY_INVENTORY_PATTERNS:
        if pattern.search(text):
            return {
                "command": text,
                "cwd": path,
                "classification": CLASSIFICATION_READ_ONLY_INVENTORY,
                "reasons": [],
            }

    for pattern in _DIAGNOSTIC_PATTERNS:
        if pattern.search(text):
            return {
                "command": text,
                "cwd": path,
                "classification": CLASSIFICATION_DIAGNOSTIC,
                "reasons": [],
            }

    if root and is_main_checkout_path(path, root):
        if re.search(r"\b(?:cat|head|less|read)\b", text, re.I):
            if re.search(r"workflow|skill|runbook", text, re.I):
                return {
                    "command": text,
                    "cwd": path,
                    "classification": CLASSIFICATION_BOUNDARY_VIOLATION,
                    "reasons": [
                        "canonical workflow viewed as local file without "
                        "gitea_load_review_workflow (narrative load is not proof)"
                    ],
                }

    return {
        "command": text,
        "cwd": path,
        "classification": CLASSIFICATION_UNCLASSIFIED,
        "reasons": [
            "pre-review command not classified; record via "
            "gitea_record_pre_review_command before workflow load"
        ],
    }


def record_pre_review_command(
    command: str,
    *,
    cwd: str | None = None,
    project_root: str | None = None,
    classification: str | None = None,
) -> dict[str, Any]:
    """Record and classify a pre-review command for the current session."""
    assessed = classify_pre_review_command(
        command, cwd=cwd, project_root=project_root)
    if classification:
        if classification not in ALLOWED_CLASSIFICATIONS:
            assessed["classification"] = CLASSIFICATION_UNCLASSIFIED
            assessed["reasons"] = [
                f"unknown classification '{classification}'; fail closed"
            ]
        else:
            assessed["classification"] = classification
            assessed["reasons"] = []
    entry = {
        **assessed,
        "session_pid": os.getpid(),
    }
    _PRE_REVIEW_COMMANDS.append(entry)
    return dict(entry)


def assess_boundary_status(project_root: str | None = None) -> dict[str, Any]:
    """Summarize pre-review boundary state for session proof and reports."""
    violations = [
        entry for entry in _PRE_REVIEW_COMMANDS
        if entry.get("classification") == CLASSIFICATION_BOUNDARY_VIOLATION
    ]
    unclassified = [
        entry for entry in _PRE_REVIEW_COMMANDS
        if entry.get("classification") == CLASSIFICATION_UNCLASSIFIED
    ]
    reasons: list[str] = []
    for entry in violations:
        reasons.extend(entry.get("reasons") or [
            f"boundary violation: {entry.get('command', '')[:80]}"
        ])
    for entry in unclassified:
        reasons.extend(entry.get("reasons") or [
            "unclassified pre-review command blocks reviewer mutations"
        ])

    clean = not reasons
    return {
        "boundary_status": "clean" if clean else "violation",
        "boundary_clean": clean,
        "pre_review_command_count": len(_PRE_REVIEW_COMMANDS),
        "boundary_violation_count": len(violations),
        "unclassified_command_count": len(unclassified),
        "violations": [
            {
                "command": v.get("command"),
                "cwd": v.get("cwd"),
                "reasons": list(v.get("reasons") or []),
            }
            for v in violations
        ],
        "reasons": reasons,
    }


def boundary_blockers(project_root: str | None = None) -> list[str]:
    """Reasons reviewer mutations must fail closed due to boundary state."""
    status = assess_boundary_status(project_root)
    if status.get("boundary_clean"):
        return []
    return list(status.get("reasons") or [
        "reviewer session boundary violation before workflow load"
    ])


def workflow_load_helper_result(
    load: dict | None,
    project_root: str | None = None,
) -> dict[str, Any]:
    """Structured helper result for final reports (#403)."""
    boundary = assess_boundary_status(project_root)
    if load is None:
        return {
            "workflow_load_proof_present": False,
            "workflow_source": None,
            "workflow_hash": None,
            "final_report_schema_hash": None,
            "boundary_status": boundary.get("boundary_status"),
            "boundary_clean": False,
            "reasons": [
                "gitea_load_review_workflow helper result missing from report"
            ],
        }
    return {
        "workflow_load_proof_present": True,
        "workflow_source": load.get("workflow_source"),
        "workflow_hash": load.get("workflow_hash"),
        "final_report_schema_path": load.get("final_report_schema_path"),
        "final_report_schema_hash": load.get("final_report_schema_hash"),
        "boundary_status": load.get("boundary_status", boundary.get("boundary_status")),
        "boundary_clean": bool(load.get("boundary_clean", boundary.get("boundary_clean"))),
        "pre_review_command_count": boundary.get("pre_review_command_count"),
        "reasons": [],
    }