"""Lease and collision visibility for the web UI (#433)."""

from __future__ import annotations

import os
import re
import subprocess
from dataclasses import dataclass
from typing import Any, Callable
from urllib.parse import urlparse

from gitea_auth import api_fetch_page, get_auth_header, repo_api_url
from issue_claim_heartbeat import build_claim_inventory
from issue_lock_provenance import ISSUE_LOCK_FILE
from merged_cleanup_reconcile import read_issue_lock

from webui.project_registry import ProjectRecord, load_registry
from webui.queue_loader import _extract_linked_issue, _fetch_issues, _fetch_prs

_REVIEWER_LEASE_MARKER = "<!-- mcp-review-lease:v1 -->"
_REVIEWER_FIELD_RE = re.compile(
    r"^\s*([a-z_]+)\s*:\s*(.+?)\s*$",
    re.IGNORECASE | re.MULTILINE,
)
_ISSUE_BRANCH_RE = re.compile(r"issue-(\d+)", re.IGNORECASE)

_COLLISION_HISTORY = (
    {"number": 267, "title": "Author work leases"},
    {"number": 268, "title": "Issue claim heartbeat leases"},
    {"number": 400, "title": "Early duplicate-work detection"},
    {"number": 407, "title": "Per-PR reviewer leases"},
)


@dataclass(frozen=True)
class CollisionWarning:
    kind: str
    message: str
    issue_number: int | None = None
    pr_numbers: tuple[int, ...] = ()


@dataclass(frozen=True)
class LeaseSnapshot:
    project_id: str
    repo_label: str
    issue_lock: dict[str, Any] | None
    claim_inventory: dict[str, Any]
    reviewer_leases: tuple[dict[str, Any], ...]
    duplicate_prs: tuple[CollisionWarning, ...]
    duplicate_branches: tuple[CollisionWarning, ...]
    collision_history: tuple[dict[str, Any], ...]
    fetch_error: str | None = None


def _repo_root() -> str:
    override = (os.environ.get("WEBUI_REPO_ROOT") or "").strip()
    if override:
        return os.path.realpath(override)
    return os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))


def _host_from_url(remote_host: str) -> str:
    parsed = urlparse(remote_host.strip())
    return parsed.netloc or remote_host.strip().rstrip("/")


def _parse_pr_ref(value: str | None) -> int | None:
    digits = re.sub(r"[^\d]", "", value or "")
    return int(digits) if digits.isdigit() else None


def parse_reviewer_lease_comment(body: str) -> dict[str, Any] | None:
    text = body or ""
    if _REVIEWER_LEASE_MARKER not in text:
        return None
    fields: dict[str, str] = {}
    for match in _REVIEWER_FIELD_RE.finditer(text):
        fields[match.group(1).strip().lower()] = match.group(2).strip()
    if not fields:
        return None
    return {
        "pr_number": _parse_pr_ref(fields.get("pr")),
        "issue_number": _parse_pr_ref(fields.get("issue")),
        "reviewer_identity": fields.get("reviewer_identity"),
        "profile": fields.get("profile"),
        "phase": (fields.get("phase") or "").strip().lower() or None,
        "expires_at": fields.get("expires_at"),
        "blocker": fields.get("blocker"),
    }


def _fetch_comments(
    host: str,
    org: str,
    repo: str,
    auth: str,
    *,
    issue_number: int,
) -> list[dict]:
    url = f"{repo_api_url(host, org, repo)}/issues/{issue_number}/comments"
    comments: list[dict] = []
    page = 1
    while page <= 10:
        raw_page, meta = api_fetch_page(url, auth, page=page, limit=50)
        comments.extend(raw_page)
        if meta.get("is_final_page"):
            break
        page += 1
    return comments


def _list_local_branch_names(project_root: str) -> list[str]:
    result = subprocess.run(
        ["git", "-C", project_root, "branch", "--list", "--format=%(refname:short)"],
        capture_output=True,
        text=True,
        check=False,
    )
    if result.returncode != 0:
        return []
    return [line.strip() for line in (result.stdout or "").splitlines() if line.strip()]


def _duplicate_pr_warnings(raw_prs: list[dict]) -> list[CollisionWarning]:
    issue_to_prs: dict[int, list[int]] = {}
    for pr in raw_prs:
        linked = _extract_linked_issue(pr.get("title"), pr.get("body"))
        if linked is None:
            continue
        issue_to_prs.setdefault(linked, []).append(int(pr["number"]))
    warnings: list[CollisionWarning] = []
    for issue_number, pr_numbers in sorted(issue_to_prs.items()):
        if len(pr_numbers) > 1:
            warnings.append(
                CollisionWarning(
                    kind="duplicate-pr",
                    issue_number=issue_number,
                    pr_numbers=tuple(sorted(pr_numbers)),
                    message=(
                        f"Issue #{issue_number} has {len(pr_numbers)} open PRs: "
                        f"{', '.join(f'#{n}' for n in sorted(pr_numbers))} (#400)"
                    ),
                )
            )
    return warnings


def _duplicate_branch_warnings(branch_names: list[str]) -> list[CollisionWarning]:
    issue_to_branches: dict[int, list[str]] = {}
    for name in branch_names:
        match = _ISSUE_BRANCH_RE.search(name)
        if not match:
            continue
        issue_num = int(match.group(1))
        issue_to_branches.setdefault(issue_num, []).append(name)
    warnings: list[CollisionWarning] = []
    for issue_number, branches in sorted(issue_to_branches.items()):
        if len(branches) > 1:
            warnings.append(
                CollisionWarning(
                    kind="duplicate-branch",
                    issue_number=issue_number,
                    message=(
                        f"Issue #{issue_number} has {len(branches)} local branches: "
                        f"{', '.join(branches)}"
                    ),
                )
            )
    return warnings


def _extract_reviewer_leases(
    raw_prs: list[dict],
    *,
    fetch_comments: Callable[[int], list[dict]],
) -> list[dict[str, Any]]:
    leases: list[dict[str, Any]] = []
    for pr in raw_prs:
        pr_number = int(pr["number"])
        for comment in fetch_comments(pr_number):
            parsed = parse_reviewer_lease_comment(comment.get("body") or "")
            if not parsed:
                continue
            leases.append(
                {
                    **parsed,
                    "pr_number": parsed.get("pr_number") or pr_number,
                    "comment_id": comment.get("id"),
                    "author": (comment.get("user") or {}).get("login"),
                    "created_at": comment.get("created_at"),
                }
            )
    active_phases = {"claimed", "validating", "approved", "request-changes", "merging"}
    return [lease for lease in leases if (lease.get("phase") or "") in active_phases]


def load_lease_snapshot(
    *,
    project_id: str | None = None,
    project_root: str | None = None,
    issue_lock_path: str | None = None,
    fetch_prs: Callable | None = None,
    fetch_issues: Callable | None = None,
    fetch_comments: Callable[[str, str, str, str, int], list[dict]] | None = None,
) -> LeaseSnapshot:
    registry = load_registry()
    project: ProjectRecord | None = None
    if project_id:
        project = next((p for p in registry.projects if p.id == project_id), None)
    else:
        project = registry.projects[0] if registry.projects else None

    root = project_root or _repo_root()
    lock = read_issue_lock(issue_lock_path if issue_lock_path is not None else ISSUE_LOCK_FILE)

    if project is None:
        return LeaseSnapshot(
            project_id=project_id or "",
            repo_label="",
            issue_lock=lock,
            claim_inventory={"entries": [], "counts": {}},
            reviewer_leases=(),
            duplicate_prs=(),
            duplicate_branches=(),
            collision_history=_COLLISION_HISTORY,
            fetch_error="project not found in registry",
        )

    host = _host_from_url(project.remote_host)
    offline_test = (os.environ.get("WEBUI_TEST_OFFLINE") or "").strip() in {
        "1",
        "true",
        "yes",
    }

    def _empty_pr_fetch(*_args, **_kwargs):
        return [], None

    def _empty_issue_fetch(*_args, **_kwargs):
        return [], None

    def _empty_comment_fetch(*_args, **_kwargs):
        return []

    pr_fetch = fetch_prs or (_empty_pr_fetch if offline_test else _fetch_prs)
    issue_fetch = fetch_issues or (
        _empty_issue_fetch if offline_test else _fetch_issues
    )
    comment_fetch = fetch_comments or (
        _empty_comment_fetch if offline_test else _fetch_comments
    )
    using_live = not offline_test and (fetch_prs is None or fetch_issues is None)
    auth = get_auth_header(host) if using_live else "test-auth"

    if using_live and not auth:
        return LeaseSnapshot(
            project_id=project.id,
            repo_label=f"{project.gitea_owner}/{project.repo_name}",
            issue_lock=lock,
            claim_inventory={"entries": [], "counts": {}},
            reviewer_leases=(),
            duplicate_prs=(),
            duplicate_branches=_duplicate_branch_warnings(_list_local_branch_names(root)),
            collision_history=_COLLISION_HISTORY,
            fetch_error=(
                f"Gitea credentials unavailable for {host}; "
                "remote lease artifacts cannot be loaded"
            ),
        )

    try:
        raw_prs, _ = pr_fetch(host, project.gitea_owner, project.repo_name, auth)
        raw_issues, _ = issue_fetch(host, project.gitea_owner, project.repo_name, auth)
    except Exception as exc:  # noqa: BLE001
        return LeaseSnapshot(
            project_id=project.id,
            repo_label=f"{project.gitea_owner}/{project.repo_name}",
            issue_lock=lock,
            claim_inventory={"entries": [], "counts": {}},
            reviewer_leases=(),
            duplicate_prs=(),
            duplicate_branches=_duplicate_branch_warnings(_list_local_branch_names(root)),
            collision_history=_COLLISION_HISTORY,
            fetch_error=f"Gitea fetch failed: {exc}",
        )

    comments_by_issue: dict[int, list[dict]] = {}
    for issue in raw_issues:
        if not any(lb.get("name") == "status:in-progress" for lb in issue.get("labels", [])):
            continue
        number = int(issue["number"])
        comments_by_issue[number] = comment_fetch(
            host, project.gitea_owner, project.repo_name, auth, issue_number=number
        )

    branch_names = _list_local_branch_names(root)
    inventory = build_claim_inventory(
        issues=raw_issues,
        comments_by_issue=comments_by_issue,
        open_prs=raw_prs,
        branch_names=branch_names,
    )

    def _pr_comments(pr_number: int) -> list[dict]:
        return comment_fetch(
            host, project.gitea_owner, project.repo_name, auth, issue_number=pr_number
        )

    reviewer_leases = tuple(_extract_reviewer_leases(raw_prs, fetch_comments=_pr_comments))

    return LeaseSnapshot(
        project_id=project.id,
        repo_label=f"{project.gitea_owner}/{project.repo_name}",
        issue_lock=lock,
        claim_inventory=inventory,
        reviewer_leases=reviewer_leases,
        duplicate_prs=tuple(_duplicate_pr_warnings(raw_prs)),
        duplicate_branches=tuple(_duplicate_branch_warnings(branch_names)),
        collision_history=_COLLISION_HISTORY,
    )


def snapshot_to_dict(snapshot: LeaseSnapshot) -> dict[str, Any]:
    return {
        "project_id": snapshot.project_id,
        "repo_label": snapshot.repo_label,
        "issue_lock": snapshot.issue_lock,
        "claim_inventory": snapshot.claim_inventory,
        "reviewer_leases": list(snapshot.reviewer_leases),
        "duplicate_prs": [
            {
                "kind": w.kind,
                "message": w.message,
                "issue_number": w.issue_number,
                "pr_numbers": list(w.pr_numbers),
            }
            for w in snapshot.duplicate_prs
        ],
        "duplicate_branches": [
            {
                "kind": w.kind,
                "message": w.message,
                "issue_number": w.issue_number,
            }
            for w in snapshot.duplicate_branches
        ],
        "collision_history": list(snapshot.collision_history),
        "fetch_error": snapshot.fetch_error,
    }
