"""Stacked-PR support for author issue locks and PR creation (#484).

Normal author work locks a worktree that is base-equivalent to ``master``/
``main``/``dev`` and opens a PR against one of those base branches. A *stacked*
PR is deliberately based on another unmerged PR's branch, so its worktree is not
master-equivalent and its PR base is not a normal base branch.

This module holds the pure decision logic that lets:

* ``gitea_lock_issue`` approve a non-master base **only** when it is explicitly
  declared and proven to correspond to an open pull request, and
* ``gitea_create_pr`` accept that approved base while still rejecting arbitrary,
  mismatched, or stale (merged/closed) branches.

The normal master-based path is unchanged: when no stacked base is declared, and
when the PR base is a normal base branch, these helpers are inert. Nothing here
bypasses the issue lock — a stacked base is recorded *on* the lock and re-checked
at PR time.
"""

from __future__ import annotations

BASE_BRANCHES = frozenset({"master", "main", "dev"})

# Phrases that satisfy the required merge-ordering statement in a stacked PR body.
MERGE_ORDER_PHRASES = ("do not merge before", "do not merge until")


def is_base_branch(base: str | None, base_branches: frozenset[str] | None = None) -> bool:
    """True when ``base`` is a normal base branch (master/main/dev)."""
    bases = base_branches or BASE_BRANCHES
    return (base or "").strip() in bases


def _pr_head_ref(pr: dict) -> str:
    head = pr.get("head") or {}
    if isinstance(head, dict):
        return (head.get("ref") or "").strip()
    return (str(head) if head else "").strip()


def find_open_pr_for_branch(open_prs: list[dict] | None, branch: str | None) -> dict | None:
    """Return the first OPEN PR whose head ref equals ``branch`` (else ``None``)."""
    branch = (branch or "").strip()
    if not branch:
        return None
    for pr in open_prs or []:
        if (pr.get("state") or "").strip().lower() != "open":
            continue
        if _pr_head_ref(pr) == branch:
            return pr
    return None


def assess_stacked_base_declaration(
    *,
    stacked_base_branch: str | None,
    stacked_base_pr: int | None,
    open_prs: list[dict] | None,
) -> dict:
    """Validate an explicit stacked-base declaration at lock time.

    Returns a dict with ``block`` (fail closed), ``reasons``, ``declared``
    (whether a stacked base was requested), and ``approved`` (the metadata to
    persist on the lock when valid, else ``None``).
    """
    branch = (stacked_base_branch or "").strip()
    if not branch:
        # No stacked base requested — normal master-based lock path.
        return {"block": False, "reasons": [], "approved": None, "declared": False}

    if branch in BASE_BRANCHES:
        return {
            "block": True,
            "declared": True,
            "approved": None,
            "reasons": [
                f"stacked base '{branch}' is already a normal base branch; do not "
                "declare a base branch as a stacked base"
            ],
        }

    if stacked_base_pr is None:
        return {
            "block": True,
            "declared": True,
            "approved": None,
            "reasons": [
                "stacked base branch declared without stacked_base_pr; a stacked PR "
                "must cite the open PR that owns the base branch"
            ],
        }

    pr = find_open_pr_for_branch(open_prs, branch)
    if pr is None:
        return {
            "block": True,
            "declared": True,
            "approved": None,
            "reasons": [
                f"stacked base branch '{branch}' does not correspond to any OPEN pull "
                "request; arbitrary or stale branches are not allowed as stacked bases"
            ],
        }

    if int(pr.get("number")) != int(stacked_base_pr):
        return {
            "block": True,
            "declared": True,
            "approved": None,
            "reasons": [
                f"declared stacked_base_pr #{stacked_base_pr} does not match the open "
                f"PR #{pr.get('number')} that owns base branch '{branch}'"
            ],
        }

    return {
        "block": False,
        "declared": True,
        "reasons": [],
        "approved": {
            "branch": branch,
            "pr_number": int(pr.get("number")),
            "verified_open": True,
        },
    }


def assess_stacked_pr_body(
    body: str | None, *, base_branch: str | None, pr_number: int | None
) -> list[str]:
    """Return the list of missing stacked-PR documentation fields (empty = ok)."""
    text = body or ""
    low = text.lower()
    missing: list[str] = []
    if base_branch and base_branch not in text:
        missing.append(f"base branch '{base_branch}'")
    if pr_number is not None and f"#{pr_number}" not in text:
        missing.append(f"stacked-on PR reference '#{pr_number}'")
    if not any(phrase in low for phrase in MERGE_ORDER_PHRASES):
        missing.append("merge-ordering statement (e.g. 'Do not merge before PR #<n>')")
    return missing


def assess_create_pr_base(
    *,
    base: str | None,
    approved_stacked_base: dict | None,
    body: str | None,
    open_prs: list[dict] | None,
    base_branches: frozenset[str] | None = None,
) -> dict:
    """Validate the PR base at create time.

    Normal base branches pass through unchanged (``stacked`` False). A non-base
    branch is allowed only when it matches the lock's approved stacked base, that
    base still has an open PR, and the body documents the stack.
    """
    bases = base_branches or BASE_BRANCHES
    base = (base or "").strip()
    if base in bases:
        return {"block": False, "reasons": [], "stacked": False}

    approved = approved_stacked_base or {}
    approved_branch = (approved.get("branch") or "").strip()
    if not approved_branch:
        return {
            "block": True,
            "stacked": True,
            "reasons": [
                f"PR base '{base}' is not one of {'/'.join(sorted(bases))} and the "
                "issue lock has no approved stacked base; re-lock with an explicit, "
                "proof-backed stacked base to open a stacked PR"
            ],
        }

    if base != approved_branch:
        return {
            "block": True,
            "stacked": True,
            "reasons": [
                f"PR base '{base}' does not match the issue lock's approved stacked "
                f"base '{approved_branch}'"
            ],
        }

    pr = find_open_pr_for_branch(open_prs, base)
    if pr is None:
        return {
            "block": True,
            "stacked": True,
            "reasons": [
                f"approved stacked base '{base}' no longer corresponds to an OPEN pull "
                "request (dependency merged, closed, or stale); retarget/rebase onto "
                "master or re-lock against a live base"
            ],
        }

    pr_number = approved.get("pr_number") or pr.get("number")
    missing = assess_stacked_pr_body(body, base_branch=base, pr_number=pr_number)
    if missing:
        return {
            "block": True,
            "stacked": True,
            "reasons": [
                "stacked PR body must document the stack; missing: "
                + ", ".join(missing)
            ],
        }

    return {"block": False, "reasons": [], "stacked": True, "stacked_base_pr": pr_number}
