"""Own-branch lock adoption / recovery for ``gitea_lock_issue`` (#442 / #443).

When an issue's own already-pushed branch exists, lock reacquisition must be
allowed (adoption) instead of being treated as #400 duplicate competing work.
This module isolates the pure decision so it can be unit-tested apart from the
MCP server's live Gitea calls.

Adoption is granted only for the issue's *exact* requested branch. Any other
branch that merely contains the same ``issue-<n>`` marker is competing work and
stays fail-closed. Open-PR, competing-live-lock, capability, and worktree
safety checks are enforced by the caller before this decision is consulted;
this module additionally records whether they passed for proof purposes.
"""

from __future__ import annotations

import re

ADOPT = "adopt_existing_branch"
BLOCK_COMPETING = "block_competing_branch"
NO_MATCH = "no_matching_branch"

# Citable decision labels aligned with the ``assess_own_branch_adoption``
# outcomes, surfaced verbatim in the live ``gitea_lock_issue`` response so
# recovery reports (#473-style) can quote the lock tool output directly
# instead of inferring adoption from separate offline checks (#477).
DECISION_LABELS = {
    ADOPT: "ADOPT",
    BLOCK_COMPETING: "BLOCK_COMPETING",
    NO_MATCH: "NO_MATCH",
}

_SAFE_NEXT_ACTIONS = {
    ADOPT: (
        "Own existing branch adopted for lock recovery; proceed to "
        "gitea_create_pr for this issue and cite this adoption proof."
    ),
    BLOCK_COMPETING: (
        "Competing same-issue branch(es) exist; resolve branch ownership "
        "before locking. No adoption performed (fail closed)."
    ),
    NO_MATCH: (
        "No existing branch carries this issue marker; normal lock path "
        "applied. No adoption performed."
    ),
}


def decision_label(outcome: str) -> str:
    """Map an ``assess_own_branch_adoption`` outcome to its citable label."""
    return DECISION_LABELS.get(outcome, "UNKNOWN")


def safe_next_action(outcome: str) -> str:
    """Return the safe next action string for an adoption *outcome*."""
    return _SAFE_NEXT_ACTIONS.get(
        outcome, "Unknown adoption outcome; treat as fail closed."
    )


def _branch_name(entry) -> str:
    if isinstance(entry, dict):
        return str(entry.get("name") or "")
    return str(entry or "")


def _branch_sha(entry) -> str | None:
    if isinstance(entry, dict):
        sha = entry.get("commit_sha")
        if sha:
            return str(sha)
    return None


def _branch_carries_issue_marker(branch_name: str, issue_number: int) -> bool:
    """Return True when *branch_name* references issue *issue_number* exactly.

    Uses a numeric word-boundary so ``issue-42`` does not match inside
    ``issue-420`` (AC6 / #440).
    """
    name = (branch_name or "").strip()
    if not name:
        return False
    pattern = rf"(?:^|/)issue-{int(issue_number)}(?![0-9])"
    return re.search(pattern, name) is not None


def branch_carries_issue_marker(branch_name: str, issue_number: int) -> bool:
    """Public accessor for the exact issue-marker match (#753).

    Dead-session lock recovery needs the same word-boundary matcher to detect
    ambiguous branch claims, so it is exposed rather than reached into.
    """
    return _branch_carries_issue_marker(branch_name, issue_number)


def assess_own_branch_adoption(
    *,
    issue_number: int,
    requested_branch: str,
    existing_branches,
) -> dict:
    """Decide whether an existing matching branch is adoptable.

    Args:
        issue_number: The tracking issue number being locked.
        requested_branch: The exact branch the caller wants to lock.
        existing_branches: Iterable of remote branch entries — either names or
            dicts with ``name`` and optional ``commit_sha``.

    Returns:
        dict with:
          * ``outcome`` — one of ADOPT / BLOCK_COMPETING / NO_MATCH
          * ``adopt`` (bool), ``block`` (bool)
          * ``reason`` (str)
          * ``matched_branch`` (str | None), ``matched_head_sha`` (str | None)
          * ``competing_branches`` (list[str])

    ADOPT: the issue's exact branch exists and no other same-issue branch does.
    BLOCK_COMPETING: at least one same-issue branch is not the requested branch.
    NO_MATCH: no branch carries the issue marker — normal lock path applies.
    """
    requested = (requested_branch or "").strip()

    matches: list[tuple[str, str | None]] = []
    for entry in existing_branches or []:
        name = _branch_name(entry).strip()
        if _branch_carries_issue_marker(name, issue_number):
            matches.append((name, _branch_sha(entry)))

    competing = sorted({name for name, _ in matches if name != requested})
    exact = [(name, sha) for name, sha in matches if name == requested]

    # Fail closed whenever any non-requested same-issue branch exists, even if
    # the requested branch is also present: ownership is then ambiguous.
    if competing:
        return {
            "outcome": BLOCK_COMPETING,
            "adopt": False,
            "block": True,
            "reason": (
                f"issue #{issue_number} already has matching branch(es) "
                f"{competing} that are not the requested branch "
                f"'{requested}' (fail closed)"
            ),
            "matched_branch": None,
            "matched_head_sha": None,
            "competing_branches": competing,
        }

    if exact:
        name, sha = exact[0]
        return {
            "outcome": ADOPT,
            "adopt": True,
            "block": False,
            "reason": (
                f"existing branch '{name}' is the exact requested branch for "
                f"issue #{issue_number}; adopting it for lock recovery"
            ),
            "matched_branch": name,
            "matched_head_sha": sha,
            "competing_branches": [],
        }

    return {
        "outcome": NO_MATCH,
        "adopt": False,
        "block": False,
        "reason": f"no existing branch matches issue #{issue_number}",
        "matched_branch": None,
        "matched_head_sha": None,
        "competing_branches": [],
    }


def _matcher_summary(issue_number: int, assessment: dict) -> str:
    """Explain, citably, why the assessed branch did or did not qualify.

    Names the numeric word-boundary rule so reports can show that
    ``issue-42`` was not matched inside ``issue-420`` (#440 / #477 AC3).
    """
    outcome = assessment.get("outcome")
    matched = assessment.get("matched_branch")
    competing = assessment.get("competing_branches") or []
    if outcome == ADOPT and matched:
        return (
            f"branch '{matched}' exactly matches the issue-{int(issue_number)} "
            f"marker (numeric word-boundary; 'issue-{int(issue_number)}' is not "
            f"matched inside 'issue-{int(issue_number)}0')"
        )
    if outcome == BLOCK_COMPETING:
        return (
            f"competing same-issue branch(es) {competing} carry the "
            f"issue-{int(issue_number)} marker but are not the requested "
            f"branch; ownership is ambiguous (fail closed)"
        )
    return (
        f"no existing branch carries the issue-{int(issue_number)} marker "
        f"under the numeric word-boundary rule"
    )


def _competing_branch_check(assessment: dict) -> dict:
    """Structured competing-branch verdict for the proof block."""
    competing = list(assessment.get("competing_branches") or [])
    return {
        "result": "blocked" if competing else "clear",
        "competing_branches": competing,
    }


def build_adoption_proof(
    *,
    issue_number: int,
    branch_name: str,
    assessment: dict,
    open_pr_checked: bool,
    competing_lock_checked: bool,
    lock_file_path: str,
    lock_file_status: str,
) -> dict:
    """Assemble the proof block returned by ``gitea_lock_issue`` on adoption.

    Requirement #4: adoption results must carry issue number, branch name,
    branch head commit, adoption reason, no-existing-PR proof, no-competing-
    live-lock proof, and lock file path/status.

    #477: additionally surface explicit, citable adoption-proof fields tied to
    the ``assess_own_branch_adoption`` outcome (``adoption_decision``,
    ``adopted``, ``adopted_branch``, ``adopted_branch_head``,
    ``matcher_summary``, ``competing_branch_check``, ``safe_next_action``) so a
    recovery session can quote the live lock response directly. The explicit
    fields are populated for any outcome; ``adopted_branch`` /
    ``adopted_branch_head`` are set only when the outcome is ADOPT so a
    non-adoption proof can never be misread as claiming adoption.
    """
    outcome = assessment.get("outcome")
    adopted = outcome == ADOPT
    return {
        "issue_number": issue_number,
        "branch_name": branch_name,
        "branch_head_commit": assessment.get("matched_head_sha"),
        "adoption_reason": assessment.get("reason"),
        "no_existing_pr_proof": bool(open_pr_checked),
        "no_competing_live_lock_proof": bool(competing_lock_checked),
        "lock_file_path": lock_file_path,
        "lock_file_status": lock_file_status,
        # Explicit citable fields (#477).
        "adoption_decision": decision_label(outcome),
        "adopted": adopted,
        "adopted_branch": branch_name if adopted else None,
        "adopted_branch_head": assessment.get("matched_head_sha") if adopted else None,
        "matcher_summary": _matcher_summary(issue_number, assessment),
        "competing_branch_check": _competing_branch_check(assessment),
        "safe_next_action": safe_next_action(outcome),
    }


def build_non_adoption_lock_proof(*, issue_number: int, branch_name: str) -> dict:
    """Safe, adoption-free proof metadata for a normal (NO_MATCH) lock.

    Requirement #477 AC2: non-adoption lock responses must stay clear and must
    not imply adoption. This returns explicit ``adopted: False`` metadata with
    the ``NO_MATCH`` decision so a normal lock response can carry citable proof
    without ever asserting a branch was adopted.
    """
    return {
        "issue_number": issue_number,
        "branch_name": branch_name,
        "adoption_decision": DECISION_LABELS[NO_MATCH],
        "adopted": False,
        "adopted_branch": None,
        "adopted_branch_head": None,
        "matcher_summary": (
            f"no existing branch carries the issue-{int(issue_number)} marker; "
            f"normal lock path (no adoption)"
        ),
        "competing_branch_check": {"result": "clear", "competing_branches": []},
        "safe_next_action": safe_next_action(NO_MATCH),
    }