"""Audit vs cleanup phase gates for reconciliation workflows (#419).

Audit/reconciliation tasks are read-only unless a separate cleanup phase is
explicitly authorized with exact capability proof, safety proof, and
before/after snapshots. Cleanup mutations must be classified in final reports;
audit reports must not claim ``no mutations`` when cleanup occurred.
"""

from __future__ import annotations

import re
from typing import Any

RECONCILE_WORKFLOW_PATH = "workflows/reconcile-landed-pr.md"

PHASE_AUDIT = "audit"
PHASE_CLEANUP = "cleanup"

# Tasks that enter audit phase on capability resolution (read-only default).
AUDIT_PHASE_TASKS = frozenset({
    "reconcile-landed-pr",
    "reconcile_landed_pr",
    "reconcile_issue_claims",
    "reconcile_merged_cleanups",
})

# Mutation tasks forbidden during audit phase (fail closed).
AUDIT_FORBIDDEN_TASKS = frozenset({
    "delete_branch",
    "create_branch",
    "push_branch",
    "create_pr",
    "commit_files",
    "gitea_commit_files",
    "mark_issue",
    "lock_issue",
    "claim_issue",
    "close_pr",
    "close_issue",
    "create_issue",
    "merge_pr",
    "review_pr",
    "submit_pr_review",
    "comment_pr",
    "comment_issue",
    "set_issue_labels",
})

# Shell/git commands audit phase must not run.
AUDIT_FORBIDDEN_COMMAND_RE = re.compile(
    r"(?:^|\s)(?:git\s+(?:push|branch\s+-D|worktree\s+remove)|"
    r"gitea_delete_branch|delete_remote_branch)",
    re.IGNORECASE,
)

_NO_MUTATIONS_RE = re.compile(
    r"(?:no\s+mutations|mutations\s*:\s*none|no\s+unsafe\s+mutation)",
    re.IGNORECASE,
)
_CLEANUP_OCCURRED_RE = re.compile(
    r"(?:delete_remote_branch|remove_local_worktree|git\s+branch\s+-D|"
    r"git\s+worktree\s+remove|remote branch.*deleted|worktree.*removed|"
    r"cleanup\s+phase\s*:\s*(?!none\b)\S)",
    re.IGNORECASE,
)
_EXTERNAL_STATE_RE = re.compile(
    r"^\s*[-*]?\s*external[- ]state mutations\s*:",
    re.IGNORECASE | re.MULTILINE,
)
_GIT_REF_RE = re.compile(
    r"^\s*[-*]?\s*git ref mutations\s*:",
    re.IGNORECASE | re.MULTILINE,
)
_CLEANUP_MUTATIONS_RE = re.compile(
    r"^\s*[-*]?\s*cleanup mutations\s*:",
    re.IGNORECASE | re.MULTILINE,
)
_CLEANUP_PHASE_AUTH_RE = re.compile(
    r"^\s*[-*]?\s*cleanup phase (?:authorized|authorization)\s*:\s*true",
    re.IGNORECASE | re.MULTILINE,
)
_DELETE_CAPABILITY_RE = re.compile(
    r"^\s*[-*]?\s*delete.?branch capability(?: proven)?\s*:\s*true",
    re.IGNORECASE | re.MULTILINE,
)
_BEFORE_AFTER_RE = re.compile(
    r"^\s*[-*]?\s*before/after (?:state )?snapshot\s*:",
    re.IGNORECASE | re.MULTILINE,
)
_SAFETY_PROOF_RE = re.compile(
    r"^\s*[-*]?\s*(?:branch|worktree) safe to remove\s*:\s*true",
    re.IGNORECASE | re.MULTILINE,
)

_session: dict[str, Any] | None = None


def _blank_session() -> dict[str, Any]:
    return {
        "phase": PHASE_AUDIT,
        "entered_from_task": None,
        "cleanup_authorized": False,
        "cleanup_authorization": {},
    }


def current_phase() -> str | None:
    """Return active reconciliation phase or None when unset."""
    if not _session:
        return None
    return _session.get("phase")


def active_record() -> dict[str, Any] | None:
    """Return a copy of the session record, if any."""
    return dict(_session) if _session else None


def clear_phase() -> None:
    """Clear reconciliation phase state."""
    global _session
    _session = None


def enter_audit_phase(task: str) -> dict[str, Any]:
    """Enter read-only audit phase for a reconciliation task."""
    global _session
    normalized = (task or "").strip().lower()
    _session = _blank_session()
    _session["entered_from_task"] = normalized
    return dict(_session)


def authorize_cleanup_phase(
    *,
    operator_approved: bool = False,
    workflow_authorized: bool = False,
    delete_capability_proven: bool = False,
    safety_proof: dict[str, Any] | None = None,
    before_after_snapshot: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Authorize cleanup phase after explicit approval and safety proofs."""
    reasons: list[str] = []
    if not (operator_approved or workflow_authorized):
        reasons.append(
            "cleanup phase requires operator approval or explicit workflow "
            "authorization"
        )
    if not delete_capability_proven:
        reasons.append(
            "cleanup phase requires exact delete_branch capability proof "
            "(gitea.branch.delete)"
        )
    safety = dict(safety_proof or {})
    if not safety.get("safe_to_delete_remote") and not safety.get(
        "safe_to_remove_worktree"
    ):
        reasons.append(
            "cleanup phase requires proof that branch/worktree is safe to remove"
        )
    snapshot = dict(before_after_snapshot or {})
    if not snapshot.get("before") or not snapshot.get("after"):
        reasons.append(
            "cleanup phase requires before/after state snapshot"
        )

    if reasons:
        return {
            "authorized": False,
            "phase": current_phase() or PHASE_AUDIT,
            "reasons": reasons,
            "safe_next_action": (
                "remain in audit-only mode or supply operator approval, "
                "delete_branch capability proof, safety proof, and "
                "before/after snapshot before cleanup"
            ),
        }

    global _session
    if _session is None:
        _session = _blank_session()
    _session["phase"] = PHASE_CLEANUP
    _session["cleanup_authorized"] = True
    _session["cleanup_authorization"] = {
        "operator_approved": operator_approved,
        "workflow_authorized": workflow_authorized,
        "delete_capability_proven": delete_capability_proven,
        "safety_proof": safety,
        "before_after_snapshot": snapshot,
    }
    return {
        "authorized": True,
        "phase": PHASE_CLEANUP,
        "reasons": [],
        "cleanup_authorization": dict(_session["cleanup_authorization"]),
        "safe_next_action": "proceed with authorized cleanup mutations only",
    }


def check_audit_task_enters_phase(task: str) -> bool:
    """Return whether resolving *task* should enter audit phase."""
    return (task or "").strip().lower() in AUDIT_PHASE_TASKS


def check_audit_mutation_allowed(task: str) -> tuple[bool, list[str]]:
    """Fail closed when a mutation task runs during audit phase."""
    normalized = (task or "").strip().lower()
    phase = current_phase()
    if phase != PHASE_AUDIT:
        return True, []
    if normalized in AUDIT_FORBIDDEN_TASKS:
        return False, [
            f"task '{normalized}' is forbidden in audit-only reconciliation "
            "mode: switch to an explicit cleanup phase with operator approval "
            "and exact delete_branch capability proof before cleanup mutations"
        ]
    return True, []


def check_cleanup_execution_allowed() -> tuple[bool, list[str]]:
    """Fail closed when cleanup execution is attempted without authorization."""
    phase = current_phase()
    if phase == PHASE_CLEANUP and (_session or {}).get("cleanup_authorized"):
        return True, []
    if phase is None:
        return False, [
            "cleanup execution requires an active reconciliation session; "
            "resolve a reconciliation audit task first"
        ]
    return False, [
        "cleanup execution forbidden in audit-only reconciliation mode; "
        "call gitea_authorize_reconciliation_cleanup_phase with operator "
        "approval, delete_branch capability proof, safety proof, and "
        "before/after snapshot"
    ]


def classify_cleanup_mutation(action: str) -> str:
    """Map a cleanup action to the required mutation ledger category (#419)."""
    normalized = (action or "").strip().lower()
    if "delete_remote" in normalized or normalized in {
        "delete_branch",
        "gitea_delete_branch",
    }:
        return "external-state"
    if "branch" in normalized and "delete" in normalized:
        return "git-ref"
    if "worktree" in normalized or "remove_local" in normalized:
        return "cleanup"
    return "cleanup"


def assess_audit_reconciliation_report(report_text: str) -> dict[str, Any]:
    """Validate audit/cleanup reconciliation reports (fail closed)."""
    text = report_text or ""
    reasons: list[str] = []

    cleanup_occurred = bool(_CLEANUP_OCCURRED_RE.search(text))
    claims_no_mutations = bool(_NO_MUTATIONS_RE.search(text))

    if cleanup_occurred and claims_no_mutations:
        reasons.append(
            "report claims no mutations but documents cleanup mutations; "
            "audit-only reports must not perform cleanup and cleanup reports "
            "must not claim no mutations"
        )

    if cleanup_occurred:
        if not _CLEANUP_PHASE_AUTH_RE.search(text):
            reasons.append(
                "cleanup mutations reported without "
                "'Cleanup phase authorized: true'"
            )
        if not _DELETE_CAPABILITY_RE.search(text):
            reasons.append(
                "cleanup mutations reported without delete_branch capability "
                "proof"
            )
        if not _BEFORE_AFTER_RE.search(text):
            reasons.append(
                "cleanup mutations reported without before/after state snapshot"
            )
        if not _SAFETY_PROOF_RE.search(text):
            reasons.append(
                "cleanup mutations reported without branch/worktree safety proof"
            )

        if re.search(r"delete_remote|remote branch.*delet", text, re.I):
            if not _EXTERNAL_STATE_RE.search(text):
                reasons.append(
                    "remote branch deletion must be classified under "
                    "External-state mutations"
                )
        if re.search(r"git\s+branch\s+-D|local branch.*delet", text, re.I):
            if not _GIT_REF_RE.search(text):
                reasons.append(
                    "local branch deletion must be classified under "
                    "Git ref mutations"
                )
        if re.search(r"worktree.*remov|remove_local_worktree", text, re.I):
            if not _CLEANUP_MUTATIONS_RE.search(text):
                reasons.append(
                    "worktree removal must be classified under Cleanup mutations"
                )

    if (
        RECONCILE_WORKFLOW_PATH.replace("workflows/", "") in text
        or "reconcile-landed-pr" in text.lower()
    ):
        if cleanup_occurred and "audit phase" in text.lower():
            if "cleanup phase" not in text.lower():
                reasons.append(
                    "report mixes audit phase with cleanup mutations without "
                    "documenting cleanup phase transition"
                )

    proven = not reasons
    return {
        "proven": proven,
        "block": not proven,
        "reasons": reasons,
        "cleanup_occurred": cleanup_occurred,
        "claims_no_mutations": claims_no_mutations,
        "safe_next_action": (
            "proceed"
            if proven
            else "fix audit/cleanup report: separate audit from cleanup phase, "
            "classify mutations, and do not claim no mutations after cleanup"
        ),
    }


def assess_audit_command_allowed(command: str) -> tuple[bool, list[str]]:
    """Block shell commands that perform cleanup during audit phase."""
    phase = current_phase()
    if phase != PHASE_AUDIT:
        return True, []
    cmd = (command or "").strip()
    if AUDIT_FORBIDDEN_COMMAND_RE.search(cmd):
        return False, [
            f"command forbidden in audit-only reconciliation mode: {cmd!r}; "
            "authorize cleanup phase before branch/worktree deletion or push"
        ]
    return True, []