"""PR-only queue cleanup mode gates and report verifier (#390).

Cleanup mode dispatches exactly one canonical review run per PR. It forbids
author-side mutations (issue claiming, branch creation, implementation edits,
issue filing) and enforces the terminal-mutation chain: stop after
``REQUEST_CHANGES``; after ``APPROVED`` continue only to same-PR merge when
merge is explicitly authorized for that PR and merge gates pass; stop after
merge or a merge blocker. The next PR always requires a fresh run.
"""

from __future__ import annotations

import re

CLEANUP_WORKFLOW_PATH = "workflows/pr-queue-cleanup.md"

# Author-side resolver tasks that must never run inside cleanup mode.
CLEANUP_FORBIDDEN_TASKS = frozenset({
    "claim_issue",
    "mark_issue",
    "lock_issue",
    "create_issue",
    "create_branch",
    "push_branch",
    "create_pr",
    "commit_files",
    "gitea_commit_files",
    "address_pr_change_requests",
})

# Run-state outcomes for one cleanup dispatch.
STOP_AFTER_REQUEST_CHANGES = "STOP_AFTER_REQUEST_CHANGES"
STOP_AFTER_DECISION = "STOP_AFTER_DECISION"
CONTINUE_TO_SAME_PR_MERGE = "CONTINUE_TO_SAME_PR_MERGE"
STOP_APPROVED_NO_MERGE_AUTH = "STOP_APPROVED_NO_MERGE_AUTH"
STOP_APPROVED_MERGE_GATES_FAILED = "STOP_APPROVED_MERGE_GATES_FAILED"
STOP_AFTER_MERGE = "STOP_AFTER_MERGE"
STOP_AFTER_MERGE_BLOCKER = "STOP_AFTER_MERGE_BLOCKER"
STOP_GATE_NOT_PROVEN = "STOP_GATE_NOT_PROVEN"

_TERMINAL_DECISIONS = frozenset({"approved", "request_changes", "comment", "skip"})


def resolve_cleanup_run_state(
    decision: str | None,
    *,
    merge_authorized_for_pr: bool = False,
    merge_gates_passed: bool | None = None,
    merge_completed: bool = False,
    merge_blocker: bool = False,
) -> dict:
    """Resolve what one cleanup run may do after its terminal review decision.

    Fail closed: unknown decisions stop the run with no further mutation.
    """
    normalized = (decision or "").strip().lower()

    if normalized not in _TERMINAL_DECISIONS:
        return {
            "outcome": STOP_GATE_NOT_PROVEN,
            "further_mutation_allowed": False,
            "reasons": [
                f"unknown terminal decision {decision!r}; cleanup run stops "
                "(fail closed)"
            ],
        }

    if normalized == "request_changes":
        return {
            "outcome": STOP_AFTER_REQUEST_CHANGES,
            "further_mutation_allowed": False,
            "reasons": ["REQUEST_CHANGES is terminal in cleanup mode"],
        }

    if normalized in {"comment", "skip"}:
        return {
            "outcome": STOP_AFTER_DECISION,
            "further_mutation_allowed": False,
            "reasons": [f"{normalized} decision ends the cleanup run"],
        }

    # normalized == "approved"
    if merge_completed:
        return {
            "outcome": STOP_AFTER_MERGE,
            "further_mutation_allowed": False,
            "reasons": ["merge completed; cleanup run stops"],
        }
    if merge_blocker:
        return {
            "outcome": STOP_AFTER_MERGE_BLOCKER,
            "further_mutation_allowed": False,
            "reasons": ["merge blocker recorded; cleanup run stops"],
        }
    if not merge_authorized_for_pr:
        return {
            "outcome": STOP_APPROVED_NO_MERGE_AUTH,
            "further_mutation_allowed": False,
            "reasons": [
                "APPROVED without explicit per-PR merge authorization; "
                "cleanup run stops"
            ],
        }
    if merge_gates_passed is not True:
        return {
            "outcome": STOP_APPROVED_MERGE_GATES_FAILED,
            "further_mutation_allowed": False,
            "reasons": [
                "merge gates not proven passed; cleanup run stops before merge"
            ],
        }
    return {
        "outcome": CONTINUE_TO_SAME_PR_MERGE,
        "further_mutation_allowed": True,
        "reasons": [],
        "allowed_mutation": "merge same PR only",
    }


def check_cleanup_task_allowed(task: str) -> tuple[bool, list[str]]:
    """Fail closed on any author-side mutation task inside cleanup mode."""
    normalized = (task or "").strip().lower()
    if normalized in CLEANUP_FORBIDDEN_TASKS:
        return False, [
            f"task '{normalized}' is forbidden in PR-only cleanup mode: "
            "no issue claiming, branch creation, implementation edits, or "
            "issue filing"
        ]
    return True, []


_SELECTED_PR_RE = re.compile(
    r"^\s*[-*]?\s*selected pr\s*:\s*#?(\d+)", re.IGNORECASE | re.MULTILINE
)
_SELECTED_PR_NONE_RE = re.compile(
    r"^\s*[-*]?\s*selected pr\s*:\s*(?:none|n/a)\b", re.IGNORECASE | re.MULTILINE
)
_EMPTY_QUEUE_RE = re.compile(
    r"\b0 open pr|\bno open pr|\bno eligible pr|\bempty (?:review )?queue|\binventory empty|\btrusted_empty\b",
    re.IGNORECASE,
)
_NEXT_SUGGESTED_RE = re.compile(
    r"^\s*[-*]?\s*next suggested pr\s*:", re.IGNORECASE | re.MULTILINE
)
_TERMINAL_MUTATION_RE = re.compile(
    r"^\s*[-*]?\s*(?:review (?:decision|verdict)|terminal (?:review )?"
    r"(?:decision|mutation))\s*:\s*"
    r"(approved|request_changes|request changes|merged|comment)",
    re.IGNORECASE | re.MULTILINE,
)
_PAGINATION_HINT_RE = re.compile(
    r"inventory_complete|final page|has_more\s*[=:]\s*false|total[_ ]count",
    re.IGNORECASE,
)
_MERGE_RESULT_RE = re.compile(
    r"^\s*[-*]?\s*merge result\s*:\s*(?!none\b|not attempted\b)\S",
    re.IGNORECASE | re.MULTILINE,
)
_MERGE_AUTHORIZED_RE = re.compile(
    r"^\s*[-*]?\s*merge authorized(?: for pr)?\s*:\s*true",
    re.IGNORECASE | re.MULTILINE,
)
_ISSUE_MUTATION_CLAIM_RE = re.compile(
    r"^\s*[-*]?\s*issue mutations\s*:\s*(?!none\b)\S",
    re.IGNORECASE | re.MULTILINE,
)
_BRANCH_MUTATION_CLAIM_RE = re.compile(
    r"^\s*[-*]?\s*branch mutations\s*:\s*(?!none\b)\S",
    re.IGNORECASE | re.MULTILINE,
)


def assess_pr_queue_cleanup_report(report_text: str) -> dict:
    """Validate a PR-only cleanup run report (single dispatch, fail closed)."""
    reasons: list[str] = []
    text = report_text or ""

    if CLEANUP_WORKFLOW_PATH not in text:
        reasons.append(
            f"report must cite the canonical cleanup workflow "
            f"({CLEANUP_WORKFLOW_PATH})"
        )

    selected = _SELECTED_PR_RE.findall(text)
    if len(selected) == 0:
        if _SELECTED_PR_NONE_RE.search(text):
            if not _EMPTY_QUEUE_RE.search(text):
                reasons.append("Selected PR is 'none' but no valid empty-queue claim found")
        else:
            reasons.append("report must name exactly one Selected PR")
    elif len(set(selected)) > 1:
        reasons.append(
            "cleanup run selected multiple PRs "
            f"({', '.join(sorted(set(selected)))}); one canonical review per "
            "PR per run"
        )

    if len(selected) > 0 and _SELECTED_PR_NONE_RE.search(text):
        reasons.append("report contains both a selected PR and a claim of none selected")

    terminal = _TERMINAL_MUTATION_RE.findall(text)
    if len(terminal) > 1:
        reasons.append(
            "multiple terminal review mutations reported in one cleanup run"
        )

    if not _PAGINATION_HINT_RE.search(text):
        reasons.append(
            "report missing PR inventory pagination proof "
            "(inventory_complete / final page / total_count)"
        )

    if not _NEXT_SUGGESTED_RE.search(text):
        reasons.append(
            "report must include 'Next suggested PR' (without continuing to it)"
        )

    if _MERGE_RESULT_RE.search(text) and not _MERGE_AUTHORIZED_RE.search(text):
        reasons.append(
            "merge reported without explicit per-PR merge authorization "
            "('Merge authorized: true')"
        )

    if _ISSUE_MUTATION_CLAIM_RE.search(text):
        reasons.append("issue mutations are forbidden in PR-only cleanup mode")
    if _BRANCH_MUTATION_CLAIM_RE.search(text):
        reasons.append("branch mutations are forbidden in PR-only cleanup mode")

    proven = not reasons
    return {
        "proven": proven,
        "block": not proven,
        "reasons": reasons,
        "safe_next_action": (
            "proceed" if proven else
            "fix the cleanup report: one PR, one terminal mutation, pagination "
            "proof, next-suggested-PR field, and no issue/branch mutations"
        ),
    }
