#!/usr/bin/env python3
"""merge_group_evidence_auditor.py — merge_group evidence freshness auditor (task-2781).
얇은 오케스트레이션: 역추적 → PR별 evaluate_gate(pr, pr_head_sha) → 재해석/집계 → check publish + exit."""
from __future__ import annotations

import argparse
import json
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT))
from gemini_evidence_verify import evaluate_gate  # noqa: E402  ★ 무수정 재사용
from merge_group_pr_resolver import resolve_merge_group_prs, MergeGroupPRsUnresolved, DEFAULT_REPO  # noqa: E402

# 상태 enum 상수 (설계서 §9)
PASS = "PASS"
MERGE_GROUP_PRS_UNRESOLVED = "MERGE_GROUP_PRS_UNRESOLVED"
GEMINI_EVIDENCE_SKIPPED_OR_MISSING = "GEMINI_EVIDENCE_SKIPPED_OR_MISSING"
GEMINI_EVIDENCE_STALE = "GEMINI_EVIDENCE_STALE"
HEAD_ADVANCED_AFTER_ADMISSION = "HEAD_ADVANCED_AFTER_ADMISSION"
UNRESOLVED_HIGH_OR_CRITICAL = "UNRESOLVED_HIGH_OR_CRITICAL"
OWNER_RESOLVE_PENDING = "OWNER_RESOLVE_PENDING"
AUDITOR_INTERNAL_ERROR = "AUDITOR_INTERNAL_ERROR"

# 집계 우선순위 (여러 PR이 동시에 실패할 때 대표 status 선택; index 작을수록 우선)
FAILURE_PRECEDENCE = [
    MERGE_GROUP_PRS_UNRESOLVED,
    AUDITOR_INTERNAL_ERROR,
    HEAD_ADVANCED_AFTER_ADMISSION,
    UNRESOLVED_HIGH_OR_CRITICAL,
    GEMINI_EVIDENCE_STALE,
    GEMINI_EVIDENCE_SKIPPED_OR_MISSING,
    OWNER_RESOLVE_PENDING,
]

CHECK_NAME = "merge-group-evidence-audit"


# --- monkeypatch seam ---

def fetch_current_pr_head(repo: str, pr_number: int) -> str | None:
    """gh api repos/{repo}/pulls/{pr_number} → head.sha 반환. 실패/파싱불가 시 None."""
    try:
        proc = subprocess.run(
            ["gh", "api", f"repos/{repo}/pulls/{pr_number}"],
            capture_output=True, text=True, timeout=30
        )
        if proc.returncode != 0 or not proc.stdout.strip():
            return None
        data = json.loads(proc.stdout)
        if not isinstance(data, dict):
            return None
        sha = data.get("head", {}).get("sha")
        if isinstance(sha, str) and sha.strip():
            return sha
        return None
    except Exception:
        return None


def fetch_owner_resolve_pending(repo: str, pr_number: int) -> bool:
    """OWNER resolve 필요 미해결 thread 존재 여부.
    기본 구현: False (신호 없으면 pending 아님).
    실제로는 review thread resolve 상태 조회 가능하나, 신호 부재=not pending으로 처리."""
    return False


def audit_pr(repo: str, pr: dict) -> dict:
    """pr={"pr_number":int,"pr_head_sha":str} 단일 PR 감사.
    반환 {"pr_number","pr_head_sha","status","reason","gate_state"}.

    판정 순서(★ 정확히 이 순서):
      1) gate = evaluate_gate(pr_number, pr_head_sha, repo)   ← ★강제1: 반드시 pr_head_sha 사용
      2) current = fetch_current_pr_head(repo, pr_number)
         - current is None → AUDITOR_INTERNAL_ERROR (fail-closed: 현재 head 확인 불가)
         - current != pr_head_sha → HEAD_ADVANCED_AFTER_ADMISSION
      3) high_severity_hits 비어있지 않음 → UNRESOLVED_HIGH_OR_CRITICAL
      4) gate.state == "block":
           primary 존재 AND 모든 primary entry 가 stale=True → GEMINI_EVIDENCE_STALE
           그 외 → GEMINI_EVIDENCE_SKIPPED_OR_MISSING
      5) gate.state == "hold" → GEMINI_EVIDENCE_SKIPPED_OR_MISSING  (★강제2: 대기 없이 fail-closed)
      6) gate.state == "pass":
           fetch_owner_resolve_pending → True 면 OWNER_RESOLVE_PENDING
           아니면 PASS
    """
    pr_number: int = pr["pr_number"]
    pr_head_sha: str = pr["pr_head_sha"]

    # 1) evaluate_gate — ★강제1: 반드시 pr_head_sha 사용 (merge_group_sha 금지)
    gate = evaluate_gate(pr_number, pr_head_sha, repo)
    gate_state = gate["state"]

    # 2) 현재 head 확인 (HEAD_ADVANCED_AFTER_ADMISSION 감지)
    current = fetch_current_pr_head(repo, pr_number)
    if current is None:
        return {
            "pr_number": pr_number,
            "pr_head_sha": pr_head_sha,
            "status": AUDITOR_INTERNAL_ERROR,
            "reason": "fetch_current_pr_head returned None — cannot confirm head SHA",
            "gate_state": gate_state,
        }
    if current != pr_head_sha:
        return {
            "pr_number": pr_number,
            "pr_head_sha": pr_head_sha,
            "status": HEAD_ADVANCED_AFTER_ADMISSION,
            "reason": f"PR head advanced after admission: admitted={pr_head_sha!r} current={current!r}",
            "gate_state": gate_state,
        }

    # 3) high_severity_hits
    high_severity_hits = gate["evidence"].get("high_severity_hits", [])
    if high_severity_hits:
        return {
            "pr_number": pr_number,
            "pr_head_sha": pr_head_sha,
            "status": UNRESOLVED_HIGH_OR_CRITICAL,
            "reason": f"Unresolved high/critical severity findings: {high_severity_hits}",
            "gate_state": gate_state,
        }

    # 4) gate.state == "block"
    if gate_state == "block":
        primary = gate["evidence"].get("primary", [])
        if primary and all(e.get("stale") is True for e in primary):
            return {
                "pr_number": pr_number,
                "pr_head_sha": pr_head_sha,
                "status": GEMINI_EVIDENCE_STALE,
                "reason": f"All primary evidence is stale (SHA mismatch): {gate['reason']}",
                "gate_state": gate_state,
            }
        return {
            "pr_number": pr_number,
            "pr_head_sha": pr_head_sha,
            "status": GEMINI_EVIDENCE_SKIPPED_OR_MISSING,
            "reason": f"Gemini evidence blocked/missing: {gate['reason']}",
            "gate_state": gate_state,
        }

    # 5) gate.state == "hold" → fail-closed (★강제2: 300초 wall-clock 대기 없이 즉시 fail)
    if gate_state == "hold":
        return {
            "pr_number": pr_number,
            "pr_head_sha": pr_head_sha,
            "status": GEMINI_EVIDENCE_SKIPPED_OR_MISSING,
            "reason": f"Gemini evidence hold interpreted as missing (fail-closed): {gate['reason']}",
            "gate_state": gate_state,
        }

    # 6) gate.state == "pass"
    if gate_state == "pass":
        pending = fetch_owner_resolve_pending(repo, pr_number)
        if pending:
            return {
                "pr_number": pr_number,
                "pr_head_sha": pr_head_sha,
                "status": OWNER_RESOLVE_PENDING,
                "reason": "Owner resolve required: unresolved review thread detected",
                "gate_state": gate_state,
            }
        return {
            "pr_number": pr_number,
            "pr_head_sha": pr_head_sha,
            "status": PASS,
            "reason": gate["reason"],
            "gate_state": gate_state,
        }

    # 알 수 없는 gate_state → fail-closed
    return {
        "pr_number": pr_number,
        "pr_head_sha": pr_head_sha,
        "status": AUDITOR_INTERNAL_ERROR,
        "reason": f"Unknown gate state: {gate_state!r}",
        "gate_state": gate_state,
    }


def audit_merge_group(
    repo: str,
    merge_group_sha: str,
    base_sha: str | None = None,
    head_ref: str | None = None,
    event: dict | None = None,
) -> dict:
    """반환 {"status", "reason", "pr_results":[...], "merge_group_sha","resolved_prs":[...]}.
      - resolve_merge_group_prs 호출. MergeGroupPRsUnresolved → status=MERGE_GROUP_PRS_UNRESOLVED.
      - resolve 중 기타 예외 → AUDITOR_INTERNAL_ERROR (fail-closed).
      - 각 PR audit_pr 루프를 try/except 로 감싸 예외 시 AUDITOR_INTERNAL_ERROR (★케이스12).
      - 집계: 모든 pr_results status==PASS 면 PASS. 아니면 실패한 것들 중 FAILURE_PRECEDENCE 최우선 status 채택.
      - resolved_prs 가 비어있으면(정상 resolve인데 0건) MERGE_GROUP_PRS_UNRESOLVED 로 fail-closed."""

    # PR 역추적
    try:
        resolved_prs = resolve_merge_group_prs(
            repo=repo,
            merge_group_sha=merge_group_sha,
            base_sha=base_sha,
            head_ref=head_ref,
            event=event,
        )
    except MergeGroupPRsUnresolved as e:
        return {
            "status": MERGE_GROUP_PRS_UNRESOLVED,
            "reason": f"PR resolution failed: {e}",
            "pr_results": [],
            "merge_group_sha": merge_group_sha,
            "resolved_prs": [],
            "diagnostics": {
                "repo": getattr(e, "repo", None) or repo,
                "merge_group_sha": getattr(e, "merge_group_sha", None) or merge_group_sha,
                "tiers": getattr(e, "diagnostics", None) or [],
            },
        }
    except Exception as e:
        return {
            "status": AUDITOR_INTERNAL_ERROR,
            "reason": f"Unexpected error during PR resolution: {e}",
            "pr_results": [],
            "merge_group_sha": merge_group_sha,
            "resolved_prs": [],
        }

    # resolved_prs가 비어있으면 fail-closed
    if not resolved_prs:
        return {
            "status": MERGE_GROUP_PRS_UNRESOLVED,
            "reason": "resolve_merge_group_prs returned empty list (0 PRs resolved)",
            "pr_results": [],
            "merge_group_sha": merge_group_sha,
            "resolved_prs": [],
            "diagnostics": {
                "repo": repo,
                "merge_group_sha": merge_group_sha,
                "tiers": [],
            },
        }

    # 각 PR 감사
    pr_results: list[dict] = []
    for pr in resolved_prs:
        try:
            result = audit_pr(repo, pr)
        except Exception as e:
            result = {
                "pr_number": pr.get("pr_number"),
                "pr_head_sha": pr.get("pr_head_sha"),
                "status": AUDITOR_INTERNAL_ERROR,
                "reason": f"audit_pr raised exception: {e}",
                "gate_state": None,
            }
        pr_results.append(result)

    # 집계
    failed = [r for r in pr_results if r["status"] != PASS]
    if not failed:
        return {
            "status": PASS,
            "reason": f"All {len(pr_results)} PR(s) passed evidence audit",
            "pr_results": pr_results,
            "merge_group_sha": merge_group_sha,
            "resolved_prs": resolved_prs,
        }

    # FAILURE_PRECEDENCE 최우선 status 선택
    failed_statuses = [r["status"] for r in failed]
    best_status = AUDITOR_INTERNAL_ERROR  # fallback
    for candidate in FAILURE_PRECEDENCE:
        if candidate in failed_statuses:
            best_status = candidate
            break

    # 해당 status의 첫 번째 실패 PR의 reason 사용
    best_reason = next(
        (r["reason"] for r in failed if r["status"] == best_status),
        f"One or more PRs failed audit: {failed_statuses}"
    )

    return {
        "status": best_status,
        "reason": best_reason,
        "pr_results": pr_results,
        "merge_group_sha": merge_group_sha,
        "resolved_prs": resolved_prs,
    }


def publish_check_run(
    repo: str,
    sha: str,
    name: str,
    conclusion: str,
    summary: str,
    details: str = "",
) -> dict:
    """GitHub check run을 gh api POST로 발행.
    gemini_review_gate.publish_check_run 와 동일 패턴."""
    payload: dict = {
        "name": name,
        "head_sha": sha,
        "status": "completed",
        "conclusion": conclusion,
        "output": {
            "title": name,
            "summary": summary[:65535],
            "text": details[:65535],
        },
    }
    proc = subprocess.run(
        ["gh", "api", "-X", "POST", f"repos/{repo}/check-runs", "--input", "-"],
        input=json.dumps(payload, ensure_ascii=False),
        capture_output=True,
        text=True,
        timeout=30,
    )
    return {"rc": proc.returncode, "stdout": proc.stdout, "stderr": proc.stderr}


def main() -> int:
    ap = argparse.ArgumentParser(
        description="merge_group_evidence_auditor — merge_group evidence freshness 감사"
    )
    ap.add_argument("--merge-group-sha", required=True, help="merge_group head SHA")
    ap.add_argument("--base-sha", default=None, help="merge_group base SHA")
    ap.add_argument("--head-ref", default=None, help="merge_group head_ref")
    ap.add_argument("--repo", default=DEFAULT_REPO, help="OWNER/REPO")
    ap.add_argument("--event-json", default=None, help="GitHub event JSON 파일 경로")
    ap.add_argument("--publish-check", action="store_true", help="GitHub check run 발행")
    ap.add_argument("--json", action="store_true", dest="json_out", help="JSON 출력")
    args = ap.parse_args()

    event: dict | None = None
    if args.event_json:
        try:
            event = json.loads(Path(args.event_json).read_text(encoding="utf-8"))
        except Exception as e:
            err = {"status": AUDITOR_INTERNAL_ERROR, "reason": f"event-json load failed: {e}"}
            print(json.dumps(err, ensure_ascii=False))
            return 1

    result = audit_merge_group(
        repo=args.repo,
        merge_group_sha=args.merge_group_sha,
        base_sha=args.base_sha,
        head_ref=args.head_ref,
        event=event,
    )

    status = result["status"]
    conclusion = "success" if status == PASS else "failure"
    summary = f"{status}: {result['reason']}"

    if args.publish_check:
        publish_check_run(
            args.repo,
            args.merge_group_sha,
            CHECK_NAME,
            conclusion,
            summary,
            json.dumps(result, ensure_ascii=False, indent=2),
        )

    if status == MERGE_GROUP_PRS_UNRESOLVED and result.get("diagnostics"):
        diag = result["diagnostics"]
        tier_summary = ",".join(
            f"{t.get('tier')}:{t.get('reject_reason')}"
            for t in diag.get("tiers", []) if t.get("invoked")
        )
        print(f"[diagnostics] repo={diag.get('repo')} tiers=[{tier_summary}]", file=sys.stderr)

    print(json.dumps(result, ensure_ascii=False, indent=2))

    return 0 if status == PASS else 1


if __name__ == "__main__":
    sys.exit(main())
