"""Live PR head re-pin before conflict-fix classification (#522).

Open PR inventory fields (mergeable, head_sha) can be stale. Author sessions
must re-fetch live PR state and pin the live head before classifying a PR as
conflicted or creating a conflict-fix worktree.
"""

from __future__ import annotations

import re
from typing import Any

_FULL_SHA = re.compile(r"^[0-9a-f]{40}$", re.IGNORECASE)

_INVENTORY_HEAD_RE = re.compile(
    r"(?:inventory head sha|stale inventory head sha)\s*:\s*([0-9a-f]{40})",
    re.IGNORECASE,
)
_LIVE_HEAD_RE = re.compile(
    r"(?:live head sha|pinned head sha|live pr head sha)\s*:\s*([0-9a-f]{40})",
    re.IGNORECASE,
)
_REPINS_TOOL_RE = re.compile(
    r"gitea_assess_conflict_fix_classification",
    re.IGNORECASE,
)
_CLASSIFICATION_RE = re.compile(
    r"conflict[- ]fix classification\s*:\s*(\S+)",
    re.IGNORECASE,
)

CLASSIFICATION_STALE_INVENTORY_SKIP = "stale_inventory_skip"
CLASSIFICATION_CONFLICT_FIX_NEEDED = "conflict_fix_needed"
CLASSIFICATION_LIVE_MERGEABLE = "live_mergeable_skip"
CLASSIFICATION_INCOMPLETE = "incomplete_live_repin"

_VALID_CLASSIFICATIONS = frozenset({
    CLASSIFICATION_STALE_INVENTORY_SKIP,
    CLASSIFICATION_CONFLICT_FIX_NEEDED,
    CLASSIFICATION_LIVE_MERGEABLE,
    CLASSIFICATION_INCOMPLETE,
})


def _normalize_sha(value: str | None) -> str | None:
    text = (value or "").strip().lower()
    if not text:
        return None
    return text if _FULL_SHA.match(text) else None


def assess_conflict_fix_classification(
    *,
    pr_number: int,
    inventory_head_sha: str | None = None,
    inventory_mergeable: bool | None = None,
    live_head_sha: str | None = None,
    live_mergeable: bool | None = None,
) -> dict[str, Any]:
    """Classify whether conflict-fix work is justified from live PR state.

    Inventory fields are advisory only. Live head + live mergeable are required
    before any conflict-fix worktree may be created.
    """
    inv_head = _normalize_sha(inventory_head_sha)
    live_head = _normalize_sha(live_head_sha)
    reasons: list[str] = []

    if not isinstance(pr_number, int) or pr_number <= 0:
        return {
            "classification": CLASSIFICATION_INCOMPLETE,
            "worktree_allowed": False,
            "skip_author_mutation": True,
            "inventory_stale_head": False,
            "inventory_stale_mergeable": False,
            "pinned_head_sha": None,
            "inventory_head_sha": inv_head,
            "live_head_sha": live_head,
            "inventory_mergeable": inventory_mergeable,
            "live_mergeable": live_mergeable,
            "pr_number": pr_number,
            "reasons": ["pr_number must be a positive integer (fail closed)"],
        }

    if live_head is None:
        reasons.append(
            "live PR head SHA missing or not a full 40-char hex SHA; "
            "re-fetch the PR before conflict-fix classification (fail closed)"
        )
    if live_mergeable is None:
        reasons.append(
            "live PR mergeable value missing; re-fetch the PR before "
            "conflict-fix classification (fail closed)"
        )

    if reasons:
        return {
            "classification": CLASSIFICATION_INCOMPLETE,
            "worktree_allowed": False,
            "skip_author_mutation": True,
            "inventory_stale_head": bool(
                inv_head and live_head and inv_head != live_head
            ),
            "inventory_stale_mergeable": (
                inventory_mergeable is not None
                and live_mergeable is not None
                and bool(inventory_mergeable) != bool(live_mergeable)
            ),
            "pinned_head_sha": live_head,
            "inventory_head_sha": inv_head,
            "live_head_sha": live_head,
            "inventory_mergeable": inventory_mergeable,
            "live_mergeable": live_mergeable,
            "pr_number": pr_number,
            "reasons": reasons,
        }

    inventory_stale_head = bool(inv_head and inv_head != live_head)
    inventory_stale_mergeable = (
        inventory_mergeable is not None
        and bool(inventory_mergeable) != bool(live_mergeable)
    )

    # Live mergeable wins: do not start conflict-fix work.
    if live_mergeable is True:
        classification = (
            CLASSIFICATION_STALE_INVENTORY_SKIP
            if (
                inventory_mergeable is False
                or inventory_stale_head
                or inventory_stale_mergeable
            )
            else CLASSIFICATION_LIVE_MERGEABLE
        )
        note = []
        if inventory_stale_head:
            note.append(
                f"inventory head {inv_head} differs from live head {live_head}; "
                "use live head only"
            )
        if inventory_mergeable is False:
            note.append(
                "inventory reported mergeable:false but live PR is mergeable:true; "
                "skip author conflict-fix mutation"
            )
        return {
            "classification": classification,
            "worktree_allowed": False,
            "skip_author_mutation": True,
            "inventory_stale_head": inventory_stale_head,
            "inventory_stale_mergeable": inventory_stale_mergeable,
            "pinned_head_sha": live_head,
            "inventory_head_sha": inv_head,
            "live_head_sha": live_head,
            "inventory_mergeable": inventory_mergeable,
            "live_mergeable": live_mergeable,
            "pr_number": pr_number,
            "reasons": note,
        }

    # live_mergeable is False → conflict-fix may proceed on the pinned live head.
    note = []
    if inventory_stale_head:
        note.append(
            f"inventory head {inv_head} differs from live head {live_head}; "
            "pin and use live head only for conflict-fix work"
        )
    if inventory_mergeable is True:
        note.append(
            "inventory reported mergeable:true but live PR is mergeable:false; "
            "trust live mergeable and proceed only with live head pin"
        )
    note.append(
        f"live PR #{pr_number} is mergeable:false at pinned head {live_head}; "
        "conflict-fix worktree allowed for that head only"
    )
    return {
        "classification": CLASSIFICATION_CONFLICT_FIX_NEEDED,
        "worktree_allowed": True,
        "skip_author_mutation": False,
        "inventory_stale_head": inventory_stale_head,
        "inventory_stale_mergeable": inventory_stale_mergeable,
        "pinned_head_sha": live_head,
        "inventory_head_sha": inv_head,
        "live_head_sha": live_head,
        "inventory_mergeable": inventory_mergeable,
        "live_mergeable": live_mergeable,
        "pr_number": pr_number,
        "reasons": note,
    }


def assess_conflict_fix_classification_final_report(
    report_text: str,
    **_kwargs: Any,
) -> dict[str, Any]:
    """Require live-head re-pin proof when a report claims conflict-fix work (#522)."""
    text = report_text or ""
    lower = text.lower()
    mentions_conflict_fix = (
        "conflict-fix" in lower
        or "conflict fix" in lower
        or "conflict_fix" in lower
    )
    if not mentions_conflict_fix:
        return {"proven": True, "reasons": [], "applicable": False}

    reasons: list[str] = []
    if not _REPINS_TOOL_RE.search(text):
        reasons.append(
            "conflict-fix report must cite gitea_assess_conflict_fix_classification "
            "(live head re-pin tool)"
        )

    live_match = _LIVE_HEAD_RE.search(text)
    if not live_match:
        reasons.append(
            "conflict-fix report must state Live head SHA: <40-char hex> "
            "(or Pinned head SHA / Live PR head SHA)"
        )
    else:
        live_sha = _normalize_sha(live_match.group(1))
        if live_sha is None:
            reasons.append("live head SHA is not a full 40-char hex digest")

    inv_match = _INVENTORY_HEAD_RE.search(text)
    # Inventory head is optional but recommended when classification is stale skip.
    class_match = _CLASSIFICATION_RE.search(text)
    if not class_match:
        reasons.append(
            "conflict-fix report must state Conflict-fix classification: "
            f"<{'|'.join(sorted(_VALID_CLASSIFICATIONS))}>"
        )
    else:
        classification = class_match.group(1).strip().lower().replace(" ", "_")
        # allow hyphenated forms
        classification = classification.replace("-", "_")
        if classification not in _VALID_CLASSIFICATIONS:
            reasons.append(
                f"unknown conflict-fix classification {class_match.group(1)!r}; "
                f"expected one of {sorted(_VALID_CLASSIFICATIONS)}"
            )

    if inv_match and live_match:
        inv_sha = _normalize_sha(inv_match.group(1))
        live_sha = _normalize_sha(live_match.group(1))
        if inv_sha and live_sha and inv_sha != live_sha:
            # Require explicit note that live head was used.
            if "use live head" not in lower and "live head only" not in lower:
                reasons.append(
                    "inventory head differs from live head; report must state that "
                    "the live head was used exclusively"
                )

    return {
        "proven": not reasons,
        "reasons": reasons,
        "applicable": True,
        "inventory_head_sha": _normalize_sha(inv_match.group(1)) if inv_match else None,
        "live_head_sha": _normalize_sha(live_match.group(1)) if live_match else None,
        "classification": (
            class_match.group(1).strip().lower().replace("-", "_").replace(" ", "_")
            if class_match
            else None
        ),
    }
