"""anu_v2.gemini_evidence_freshness_checker — task-2641 신규 helper.

회장 verbatim 12 (2026-05-23) §10 1:1 박제: Gemini fresh evidence 판정 =
``PR current HEAD SHA == Gemini review commit_id`` (spec §3.1).

task-2640 사고 박제 (spec §3.1):
  - 회장 review id=4350214991 state=COMMENTED · body 빈 문자열 → ``gemini-code-assist[bot]``
    author 가 아니므로 Gemini review 로 인정 0
  - PR HEAD 변경 후 fresh Gemini review commit_id 미도착 → STALE
  - STALE 자동 감지 → 1차 OWNER nudge 발사 → fail 시 CHAIR_UI_FALLBACK_REQUIRED

본 모듈 책임 (spec §3.1, task md §회장 verbatim 12 #10):
  - 입력: pr_number / current_head_sha / github_api callable
  - 출력: FreshnessResult enum (FRESH / STALE / NO_REVIEW)
  - PR HEAD 와 Gemini review commit_id 정합 검증

one-way isolation: anu_v2/ 외부 import 금지. live cokacdir / gh CLI 호출 0
(github_api 는 호출자가 inject; regression mock 가능).
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable, Final


# 회장 verbatim §10: fresh review 판정 결과 enum
RESULT_FRESH: Final[str] = "FRESH"
RESULT_STALE: Final[str] = "STALE"
RESULT_NO_REVIEW: Final[str] = "NO_REVIEW"

ALL_RESULTS: Final[frozenset[str]] = frozenset(
    {RESULT_FRESH, RESULT_STALE, RESULT_NO_REVIEW}
)

# Gemini bot author login (회장 verbatim §1/§2/§3 — PR Review 가 아닌
# PR Conversation issue comment 와 별개로, fresh review 의 author 는 bot 임)
GEMINI_BOT_LOGIN: Final[str] = "gemini-code-assist[bot]"

# task-2640 사고 박제: 회장 UI 입력은 owner 본인 review (state=COMMENTED · empty body)
# 가 Gemini fresh review 로 오인되는 것을 차단. spec §2.1.
CHAIR_REVIEW_ID_BLOCKED_FIXTURE: Final[int] = 4350214991


class FreshnessCheckerError(RuntimeError):
    """입력 schema 위반 (head SHA 형식 / github_api 호출 결과)."""


@dataclass(frozen=True)
class FreshnessResult:
    """check_gemini_evidence_fresh 결과 객체.

    fields:
      - status: ``FRESH`` / ``STALE`` / ``NO_REVIEW``
      - pr_number: 평가된 PR 번호
      - current_head_sha: PR 의 실측 HEAD (입력 그대로, lower 정규화)
      - gemini_commit_id_observed: 관측된 가장 최근 Gemini review commit_id
        (``NO_REVIEW`` 시 None)
      - reviews_inspected: 검사한 review 항목 수 (진단용)
      - reason: 분류 사유 (진단용)
    """

    status: str
    pr_number: int
    current_head_sha: str
    gemini_commit_id_observed: str | None
    reviews_inspected: int
    reason: str


def _normalise_head(head: Any) -> str:
    if not isinstance(head, str) or len(head) != 40:
        raise FreshnessCheckerError(
            f"head must be 40-char hex SHA string, got {head!r}"
        )
    lowered = head.lower()
    if any(c not in "0123456789abcdef" for c in lowered):
        raise FreshnessCheckerError(
            f"head must be 40-char hex SHA, got {head!r}"
        )
    return lowered


def _is_gemini_review(review: Any) -> bool:
    """회장 verbatim §1/§2: PR Review comment 는 Gemini trigger 가 아니지만,
    *Gemini bot 이 직접 post 한 review* 는 commit_id 추출용으로 검사한다.

    빈 body / 회장 본인 review 등은 author login 으로 자동 배제.
    """
    if not isinstance(review, dict):
        return False
    user = review.get("user") or review.get("author") or {}
    if not isinstance(user, dict):
        return False
    login = user.get("login") or user.get("name") or ""
    if not isinstance(login, str):
        return False
    return login.strip().lower() == GEMINI_BOT_LOGIN.lower()


def _extract_commit_id(review: Any) -> str | None:
    """review dict 에서 commit_id 회수 (lower 정규화)."""
    if not isinstance(review, dict):
        return None
    commit_id = review.get("commit_id") or review.get("commit_sha")
    if not isinstance(commit_id, str) or not commit_id:
        return None
    cid_norm = commit_id.lower()
    if len(cid_norm) != 40 or any(c not in "0123456789abcdef" for c in cid_norm):
        return None
    return cid_norm


def check_gemini_evidence_fresh(
    *,
    pr_number: int,
    current_head_sha: str,
    github_api: Callable[[str, str], Any],
    reviews_path_template: str = "/repos/{owner}/{repo}/pulls/{pr}/reviews",
    owner: str = "",
    repo: str = "",
) -> FreshnessResult:
    """Gemini fresh evidence 판정. spec §3.1 1:1.

    Args:
      pr_number: 평가 대상 PR 번호 (positive int).
      current_head_sha: 실측 PR HEAD SHA (40-char hex).
      github_api: ``Callable[(method, path), payload]`` — REST 호출 추상화.
        regression mock 가능.
      reviews_path_template: reviews endpoint template. {owner}/{repo}/{pr}
        치환. spec §2.6 1:1 — gh api 또는 동등 안전 경로.
      owner: GitHub owner (없을 시 빈 문자열 — github_api 가 처리).
      repo: GitHub repo (없을 시 빈 문자열).

    Returns:
      FreshnessResult — status (FRESH/STALE/NO_REVIEW) + commit_id observed.

    Raises:
      FreshnessCheckerError: 입력 schema 위반.
    """
    if not isinstance(pr_number, int) or isinstance(pr_number, bool) or pr_number <= 0:
        raise FreshnessCheckerError("pr_number must be a positive int")
    head_norm = _normalise_head(current_head_sha)

    path = reviews_path_template.format(
        owner=owner or "OWNER",
        repo=repo or "REPO",
        pr=pr_number,
    )

    try:
        payload = github_api("GET", path)
    except Exception as exc:  # noqa: BLE001 — caller injects mock; bubble for diag
        raise FreshnessCheckerError(
            f"github_api invocation failed for {path!r}: {exc!r}"
        ) from exc

    if payload is None:
        # github_api 가 None 반환 — review 데이터 없음으로 간주
        return FreshnessResult(
            status=RESULT_NO_REVIEW,
            pr_number=pr_number,
            current_head_sha=head_norm,
            gemini_commit_id_observed=None,
            reviews_inspected=0,
            reason="github_api returned None — no review data",
        )

    if not isinstance(payload, list):
        raise FreshnessCheckerError(
            f"reviews payload must be a list, got {type(payload).__name__}"
        )

    gemini_commit_ids: list[str] = []
    for review in payload:
        if not _is_gemini_review(review):
            continue
        cid = _extract_commit_id(review)
        if cid is None:
            continue
        gemini_commit_ids.append(cid)

    if not gemini_commit_ids:
        return FreshnessResult(
            status=RESULT_NO_REVIEW,
            pr_number=pr_number,
            current_head_sha=head_norm,
            gemini_commit_id_observed=None,
            reviews_inspected=len(payload),
            reason=(
                f"no Gemini bot review (author={GEMINI_BOT_LOGIN}) with "
                "valid commit_id found"
            ),
        )

    # 가장 최근 Gemini review = 리스트 끝 (GitHub API order). spec §3.1.
    latest_commit_id = gemini_commit_ids[-1]
    if latest_commit_id == head_norm:
        return FreshnessResult(
            status=RESULT_FRESH,
            pr_number=pr_number,
            current_head_sha=head_norm,
            gemini_commit_id_observed=latest_commit_id,
            reviews_inspected=len(payload),
            reason=(
                f"Gemini latest review commit_id matches PR HEAD ({head_norm[:8]}...)"
            ),
        )
    return FreshnessResult(
        status=RESULT_STALE,
        pr_number=pr_number,
        current_head_sha=head_norm,
        gemini_commit_id_observed=latest_commit_id,
        reviews_inspected=len(payload),
        reason=(
            f"PR HEAD ({head_norm[:8]}...) != latest Gemini review "
            f"commit_id ({latest_commit_id[:8]}...) — STALE"
        ),
    )


def is_gemini_trigger_comment(comment: Any) -> bool:
    """회장 verbatim §1/§2/§3 — PR-backed issue comment body="/gemini review"
    만 trigger 로 인정.

    spec §2.1 / §2.2 1:1 박제:
      - comment.kind == "issue_comment" (NOT "review_comment" / "review")
      - comment.body.strip() == "/gemini review"
      - author 가 owner allowlist 안 (호출자 enforce)

    task-2640 사고 박제: 회장 review id=4350214991 state=COMMENTED · empty body
    → kind="review" + body="" → False.
    """
    if not isinstance(comment, dict):
        return False
    kind = comment.get("kind") or comment.get("comment_type") or ""
    if not isinstance(kind, str):
        return False
    if kind.strip().lower() != "issue_comment":
        return False
    body = comment.get("body")
    if not isinstance(body, str):
        return False
    return body.strip() == "/gemini review"


__all__ = [
    "RESULT_FRESH",
    "RESULT_STALE",
    "RESULT_NO_REVIEW",
    "ALL_RESULTS",
    "GEMINI_BOT_LOGIN",
    "CHAIR_REVIEW_ID_BLOCKED_FIXTURE",
    "FreshnessResult",
    "FreshnessCheckerError",
    "check_gemini_evidence_fresh",
    "is_gemini_trigger_comment",
]
