"""PR Open Watcher Auto-Wrapper — task-2643 Track C (dry-run only · live 호출 0).

목적: PR open 직후 ANU 본체가 `gh pr view` 폴링 욕구를 갖지 않도록,
PR 생성 + watcher_contract_v1 자동 발급 + watcher dispatch 등록을 한 함수로 묶는다.

본 모듈은 **dry-run 만 수행**한다 (회장 verbatim):
- live `gh pr create` 호출 0
- live `cokacdir --cron` 호출 0
- 모든 외부 효과는 mock callable 로 주입받는다 (regression test 친화).

호출 사이트가 ANU 본체일 때:
- watcher_owner 가 ANU 본체 식별자면 즉시 fail (owner 위장 차단).
- collector_role 이 "ANU" 가 아니면 fail (collector_role 위장 차단).
- 위 검사 통과 시 contract 생성 + (mock) dispatch → schedule_id 반환.

bzaona6au 사건 (PR #145) 재현 fixture 를 fixture 폴더에서 동일 wrapper 로
재현하여 fail-closed 동작을 박제한다.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Callable, Optional

# ─────────────────────────────────────────────────────────────────────────────
# 상수
# ─────────────────────────────────────────────────────────────────────────────

ANU_OWNER_KEY = "c119085addb0f8b7"

# owner 가 ANU 본체로 위장한 패턴 (case-sensitive 4종 + lowercase)
ANU_SELF_IDENTIFIERS = {
    "anu",
    "ANU",
    "anu_main",
    "ANU_MAIN",
    "anu_session",
    "ANU_SESSION",
}

ALLOWED_COLLECTOR_ROLE = "ANU"

DEFAULT_TERMINAL_STATES = (
    "MERGE_READY",
    "CHAIR_REQUIRED",
    "GEMINI_EXTERNAL_TRIGGER_STALE",
    "CI_FAILED_NON_REMEDIABLE",
    "LOOP_BOUNDARY",
)

DEFAULT_ENVELOPE_AXES = (
    "delivery_outcome",
    "miss_cause",
    "root_cause_tags",
    "canonical_root",
    "registration_status",
)

DEFAULT_TTL_SECONDS = 7200

CONTRACT_SCHEMA_ID = "anu.watcher_contract.v1"


# ─────────────────────────────────────────────────────────────────────────────
# 에러
# ─────────────────────────────────────────────────────────────────────────────


class PrOpenWatcherWrapperError(Exception):
    """공통 베이스."""


class OwnerImpersonationError(PrOpenWatcherWrapperError):
    """owner 가 ANU 본체 식별자로 위장한 경우."""


class CollectorRoleViolationError(PrOpenWatcherWrapperError):
    """collector_role 위장 (anu_watcher / anu_polling 등)."""


class LiveCallViolationError(PrOpenWatcherWrapperError):
    """dry_run=True 인데 live mock 이 실제 호출을 시도한 경우 (mock 가드)."""


# ─────────────────────────────────────────────────────────────────────────────
# 결과 dataclass
# ─────────────────────────────────────────────────────────────────────────────


@dataclass(frozen=True)
class WrapperResult:
    pr_number: int
    head_sha: str
    watcher_schedule_id: str
    watcher_owner: str
    contract: dict[str, Any]
    dry_run: bool = True
    live_calls_emitted: int = 0
    rejection_reason: Optional[str] = None
    rejection_detail: Optional[str] = None
    notes: list[str] = field(default_factory=list)


# ─────────────────────────────────────────────────────────────────────────────
# 내부 검증
# ─────────────────────────────────────────────────────────────────────────────


def _validate_owner_not_anu(owner: str) -> None:
    if not owner or not owner.strip():
        raise OwnerImpersonationError("owner must be non-empty string")
    if owner.strip() in ANU_SELF_IDENTIFIERS:
        raise OwnerImpersonationError(
            f"owner='{owner}' is an ANU self-identifier — ANU 본체 위장 폴링 차단"
        )


def _validate_collector_role(collector_role: str) -> None:
    if collector_role != ALLOWED_COLLECTOR_ROLE:
        raise CollectorRoleViolationError(
            f"collector_role='{collector_role}' 위장 폴링. ALLOWED={ALLOWED_COLLECTOR_ROLE!r}"
        )


def _build_contract(
    task_id: str,
    pr_number: int,
    head_sha: str,
    owner: str,
    ttl_seconds: int,
    watcher_owner_kind: str,
) -> dict[str, Any]:
    return {
        "schema": CONTRACT_SCHEMA_ID,
        "task_id": task_id,
        "pr_number": pr_number,
        "head_sha": head_sha,
        "terminal_states": list(DEFAULT_TERMINAL_STATES),
        "ttl_seconds": ttl_seconds,
        "callback_target": {
            "type": "ANU_NORMAL_CALLBACK",
            "owner_key": ANU_OWNER_KEY,
            "envelope_axes": list(DEFAULT_ENVELOPE_AXES),
        },
        "duplicate_policy": "DEDUPE_ON_PR_HEAD_SHA",
        "owner": owner,
        "collector_role": ALLOWED_COLLECTOR_ROLE,
        "callback_only_reporting": True,
        "watcher_owner_kind": watcher_owner_kind,
    }


# ─────────────────────────────────────────────────────────────────────────────
# 공개 진입점
# ─────────────────────────────────────────────────────────────────────────────


def open_pr_and_dispatch_watcher(
    *,
    task_id: str,
    branch: str,
    title: str,
    body: str,
    owner: str,
    watcher_owner_kind: str = "dev_bot",
    collector_role: str = ALLOWED_COLLECTOR_ROLE,
    ttl_seconds: int = DEFAULT_TTL_SECONDS,
    gh_pr_create: Callable[..., dict[str, Any]],
    watcher_dispatch: Callable[..., dict[str, Any]],
    dry_run: bool = True,
) -> WrapperResult:
    """PR open + watcher dispatch 를 묶어 ANU 가 직접 polling 할 욕구를 없앤다.

    Args:
        task_id: 발사 주체 task id (예: task-2643).
        branch: PR head branch name.
        title: PR title.
        body: PR body.
        owner: watcher 발사 주체 (dev bot name 또는 cron-watcher-*).
        watcher_owner_kind: "dev_bot" | "cron_watcher".
        collector_role: callback collector role. "ANU" 만 허용.
        ttl_seconds: watcher TTL (sec).
        gh_pr_create: gh pr create 호출자 (regression 에선 mock).
            반환: {"pr_number": int, "head_sha": str (40hex)}
        watcher_dispatch: watcher dispatch 호출자 (regression 에선 mock).
            반환: {"schedule_id": str}
        dry_run: True 면 mock 만 호출. False (live) 는 본 task 범위에서 호출 0.

    Returns:
        WrapperResult.
    """
    if dry_run is False:
        # 본 task 범위에서 live 호출 0 (회장 verbatim acceptance)
        raise LiveCallViolationError(
            "dry_run=False is forbidden in task-2643 scope (live `gh pr create` 호출 0)"
        )

    # 1) owner / collector_role 위장 검사 (PR open 전 fail-closed)
    _validate_owner_not_anu(owner)
    _validate_collector_role(collector_role)

    # 2) gh pr create (mock)
    pr_result = gh_pr_create(branch=branch, title=title, body=body, dry_run=True)
    if not isinstance(pr_result, dict):
        raise PrOpenWatcherWrapperError(
            f"gh_pr_create must return dict, got {type(pr_result).__name__}"
        )
    pr_number = int(pr_result["pr_number"])
    head_sha = str(pr_result["head_sha"])
    if len(head_sha) != 40:
        raise PrOpenWatcherWrapperError(
            f"head_sha must be 40 hex chars, got len={len(head_sha)}"
        )

    # 3) contract 자동 생성 (9 필드)
    contract = _build_contract(
        task_id=task_id,
        pr_number=pr_number,
        head_sha=head_sha,
        owner=owner,
        ttl_seconds=ttl_seconds,
        watcher_owner_kind=watcher_owner_kind,
    )

    # 4) watcher dispatch (mock)
    dispatch_result = watcher_dispatch(contract=contract, dry_run=True)
    if not isinstance(dispatch_result, dict):
        raise PrOpenWatcherWrapperError(
            f"watcher_dispatch must return dict, got {type(dispatch_result).__name__}"
        )
    schedule_id = str(dispatch_result["schedule_id"])
    if not schedule_id:
        raise PrOpenWatcherWrapperError("watcher_dispatch returned empty schedule_id")

    notes = [
        f"PR open + watcher dispatch completed in dry-run (live calls=0).",
        f"watcher contract head_sha={head_sha[:8]} pr=#{pr_number} ttl={ttl_seconds}s.",
        f"ANU 본체는 본 PR 의 CI/Gemini 를 직접 polling 하지 않는다.",
    ]

    return WrapperResult(
        pr_number=pr_number,
        head_sha=head_sha,
        watcher_schedule_id=schedule_id,
        watcher_owner=owner,
        contract=contract,
        dry_run=True,
        live_calls_emitted=0,
        rejection_reason=None,
        rejection_detail=None,
        notes=notes,
    )
