"""Already-landed PR reconciliation workflow helpers (#301).

Read-only assessment and capability planning for reconciling open PRs whose
heads are already ancestors of the target branch. Does not invoke review or
merge paths.
"""

from __future__ import annotations

import subprocess
from typing import Any

import gitea_config
from merged_cleanup_reconcile import extract_linked_issue, is_head_ancestor_of_ref

ELIGIBILITY_ALREADY_LANDED = "ALREADY_LANDED_RECONCILE_REQUIRED"
ELIGIBILITY_NOT_LANDED = "NOT_ALREADY_LANDED"
ELIGIBILITY_STALE_TARGET = "TARGET_BRANCH_UNVERIFIED"
ELIGIBILITY_PR_NOT_OPEN = "PR_NOT_OPEN"

OUTCOME_FULL_RECONCILE = "FULL_RECONCILE_CLOSE_ALLOWED"
OUTCOME_PARTIAL_COMMENT = "PARTIAL_RECONCILE_COMMENT_THEN_STOP"
OUTCOME_RECOVERY_HANDOFF = "RECOVERY_HANDOFF_ONLY"
OUTCOME_NOT_LANDED = "NOT_LANDED_NO_ACTION"
OUTCOME_GATE_NOT_PROVEN = "GATE_NOT_PROVEN"

RECONCILE_WORKFLOW_MARKERS = (
    "workflows/reconcile-landed-pr.md",
    "reconcile-landed-pr.md",
)

RECONCILE_TASK_MARKERS = (
    "reconcile-landed-pr",
    "reconcile already-landed",
    "reconcile_already_landed",
)


def fetch_target_branch(project_root: str, remote: str, branch: str) -> dict[str, Any]:
    """Fetch *branch* from *remote* and return the resolved SHA."""
    ref = f"{remote}/{branch}"
    fetch = subprocess.run(
        ["git", "-C", project_root, "fetch", remote, branch],
        capture_output=True,
        text=True,
        check=False,
    )
    if fetch.returncode != 0:
        return {
            "success": False,
            "target_branch": branch,
            "target_ref": ref,
            "target_branch_sha": None,
            "reasons": [
                f"git fetch {remote} {branch} failed: "
                f"{(fetch.stderr or fetch.stdout or '').strip()}"
            ],
        }

    rev = subprocess.run(
        ["git", "-C", project_root, "rev-parse", ref],
        capture_output=True,
        text=True,
        check=False,
    )
    if rev.returncode != 0:
        return {
            "success": False,
            "target_branch": branch,
            "target_ref": ref,
            "target_branch_sha": None,
            "reasons": [
                f"git rev-parse {ref} failed: "
                f"{(rev.stderr or rev.stdout or '').strip()}"
            ],
        }

    return {
        "success": True,
        "target_branch": branch,
        "target_ref": ref,
        "target_branch_sha": (rev.stdout or "").strip(),
        "reasons": [],
        "git_fetch_command": f"git fetch {remote} {branch}",
    }


def profile_reconciliation_capabilities(
    allowed: list[str] | None,
    forbidden: list[str] | None,
) -> dict[str, bool]:
    """Map active profile operations to reconciliation capabilities."""
    allowed = allowed or []
    forbidden = forbidden or []

    def can(op: str) -> bool:
        return gitea_config.check_operation(op, allowed, forbidden)[0]

    return {
        "read": can("gitea.read"),
        "comment_pr": can("gitea.pr.comment"),
        "comment_issue": can("gitea.issue.comment"),
        "close_pr": can("gitea.pr.close"),
        "close_issue": can("gitea.issue.close"),
        "review_pr": can("gitea.pr.review") or can("gitea.pr.approve"),
        "merge_pr": can("gitea.pr.merge"),
    }


def assess_open_pr_reconciliation(
    *,
    pr: dict[str, Any],
    project_root: str,
    remote: str,
    target_branch: str,
    target_fetch: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Assess whether an open PR is eligible for already-landed reconciliation."""
    pr_number = int(pr.get("number") or 0)
    pr_state = (pr.get("state") or "").strip().lower()
    head = pr.get("head") or {}
    head_sha = head.get("sha") if isinstance(head, dict) else None
    head_ref = head.get("ref") if isinstance(head, dict) else None
    base = pr.get("base") or {}
    base_ref = base.get("ref") if isinstance(base, dict) else None
    title = pr.get("title") or ""
    body = pr.get("body") or ""

    fetch_result = target_fetch or fetch_target_branch(
        project_root, remote, target_branch
    )
    linked_issue = extract_linked_issue(title, body)

    result: dict[str, Any] = {
        "pr_number": pr_number,
        "pr_state": pr_state,
        "candidate_head_sha": head_sha,
        "head_ref": head_ref,
        "base_ref": base_ref or target_branch,
        "target_branch": target_branch,
        "target_branch_sha": fetch_result.get("target_branch_sha"),
        "linked_issue": linked_issue,
        "git_ref_mutations": [],
        "reasons": [],
        "review_merge_allowed": False,
    }
    if fetch_result.get("git_fetch_command"):
        result["git_ref_mutations"].append(fetch_result["git_fetch_command"])

    if pr_state != "open":
        result["eligibility_class"] = ELIGIBILITY_PR_NOT_OPEN
        result["ancestor_proof"] = None
        result["reconciliation_allowed"] = False
        result["reasons"].append(f"PR #{pr_number} state is {pr_state!r}, not open")
        return result

    if not fetch_result.get("success"):
        result["eligibility_class"] = ELIGIBILITY_STALE_TARGET
        result["ancestor_proof"] = None
        result["reconciliation_allowed"] = False
        result["reasons"].extend(fetch_result.get("reasons") or [])
        return result

    target_ref = fetch_result.get("target_ref") or f"{remote}/{target_branch}"
    ancestor = is_head_ancestor_of_ref(project_root, head_sha, target_ref)
    result["ancestor_proof"] = ancestor

    if ancestor is None:
        result["eligibility_class"] = ELIGIBILITY_STALE_TARGET
        result["reconciliation_allowed"] = False
        result["reasons"].append(
            f"ancestor check failed for head {head_sha!r} against {target_ref}"
        )
        return result

    if ancestor:
        result["eligibility_class"] = ELIGIBILITY_ALREADY_LANDED
        result["reconciliation_allowed"] = True
        return result

    result["eligibility_class"] = ELIGIBILITY_NOT_LANDED
    result["reconciliation_allowed"] = False
    result["reasons"].append(
        f"PR head {head_sha} is not an ancestor of {target_ref}"
    )
    return result


def resolve_reconciliation_plan(
    *,
    assessment: dict[str, Any],
    capabilities: dict[str, bool] | None,
) -> dict[str, Any]:
    """Map eligibility + profile capabilities to a reconciliation outcome."""
    caps = capabilities or {}
    reasons: list[str] = []

    if not assessment.get("reconciliation_allowed"):
        outcome = OUTCOME_NOT_LANDED
        if assessment.get("eligibility_class") in {
            ELIGIBILITY_STALE_TARGET,
            ELIGIBILITY_PR_NOT_OPEN,
        }:
            outcome = OUTCOME_GATE_NOT_PROVEN
        reasons.extend(assessment.get("reasons") or [])
        return {
            "outcome": outcome,
            "close_pr_allowed": False,
            "comment_pr_allowed": False,
            "close_issue_allowed": False,
            "comment_issue_allowed": False,
            "review_merge_allowed": False,
            "missing_capabilities": _missing_reconciliation_capabilities(caps),
            "safe_next_action": (
                "Do not reconcile via review/merge; repair missing proof first."
            ),
            "reasons": reasons,
        }

    close_pr = bool(caps.get("close_pr"))
    comment_pr = bool(caps.get("comment_pr"))
    close_issue = bool(caps.get("close_issue"))
    comment_issue = bool(caps.get("comment_issue"))

    if caps.get("review_pr") or caps.get("merge_pr"):
        reasons.append(
            "reconciliation path must not use review/merge capabilities"
        )

    if close_pr:
        outcome = OUTCOME_FULL_RECONCILE
        safe_next_action = (
            "Run reconciliation close via exact gitea.pr.close after proof; "
            "do not approve or merge."
        )
    elif comment_pr:
        outcome = OUTCOME_PARTIAL_COMMENT
        safe_next_action = (
            "Post one reconciliation comment with ancestor proof, then stop "
            "for an authorized close profile."
        )
    else:
        outcome = OUTCOME_RECOVERY_HANDOFF
        safe_next_action = (
            "Produce a recovery handoff naming missing gitea.pr.close and/or "
            "gitea.pr.comment; do not loop through review/merge."
        )
        reasons.append("gitea.pr.close is not available in the active profile")

    return {
        "outcome": outcome,
        "close_pr_allowed": close_pr,
        "comment_pr_allowed": comment_pr,
        "close_issue_allowed": close_issue,
        "comment_issue_allowed": comment_issue,
        "review_merge_allowed": False,
        "missing_capabilities": _missing_reconciliation_capabilities(caps),
        "safe_next_action": safe_next_action,
        "reasons": reasons,
    }


def _missing_reconciliation_capabilities(caps: dict[str, bool]) -> list[str]:
    missing = []
    if not caps.get("read"):
        missing.append("gitea.read")
    if not caps.get("close_pr"):
        missing.append("gitea.pr.close")
    if not caps.get("comment_pr"):
        missing.append("gitea.pr.comment")
    return missing


def assess_reconcile_workflow_source(report_text: str) -> dict[str, Any]:
    """Reconciliation reports must cite the canonical workflow file."""
    lower = (report_text or "").lower()
    reasons = []
    if not any(marker in lower for marker in RECONCILE_WORKFLOW_MARKERS):
        reasons.append(
            "reconciliation report missing workflow source "
            "(workflows/reconcile-landed-pr.md)"
        )
    if not any(marker in lower for marker in RECONCILE_TASK_MARKERS):
        reasons.append(
            "reconciliation report missing task mode declaration "
            "(reconcile-landed-pr)"
        )
    return {
        "complete": not reasons,
        "downgraded": bool(reasons),
        "reasons": reasons,
    }