"""conti_ab — 콘티 사용/미사용 A/B 외부 판정 (E2E6 피드백 ⑧, 2026-07-16).

일반(non-lane·non-prev·비 bgonly) 콘티 사용 샷에서 A(콘티 참조 포함)/
B(미포함) 두 완결 파이프 산출(_sel)을 outer VLM 이 비교해 우세본을
채택한다. Codex 설계 합의:
  - 첫 production acceptance = 풀 3롤×2 (1롤 프로브는 생성 분산을 모드
    효과로 오인 — 부적합).
  - 블라인드(내부 선택지 노출 금지) + 입력 순서 뒤집기 2회 판정 —
    불일치=동점, 동점=콘티 사용본(기존 파이프 연속성).
  - 1차 범위=일반 shot_conti_light 콘티 샷만 (lane1 스케치=동등한
    무스케치 앵커 없어 불공정, lane2 direct_seed=스케치 자체 없음).
판정 계약=프레이밍 최상위(multiroll_judge v2 와 동일 원칙, S23sh1 교훈).
"""
from __future__ import annotations

import logging
from pathlib import Path
from typing import Any, Callable, Dict, Optional

from app.modules.prompt_loader import load_prompt, load_schema

logger = logging.getLogger(__name__)

_MODULE = "conti_ab_judge"
AB_JUDGE_MODEL = "gemini-pro"

PROMPT_VERSION_MAP = {
    "1": "1.202607161800",
}


def resolve_prompt_version(selector: str) -> str:
    try:
        return PROMPT_VERSION_MAP[selector]
    except KeyError:
        raise ValueError(
            f"unknown conti_ab_judge prompt version selector "
            f"{selector!r} (known: {sorted(PROMPT_VERSION_MAP)})"
        )


def decide_winner(verdicts: list) -> str:
    """순서 뒤집기 2회 판정 → 'A'|'B'|'tie' (불일치=tie).

    verdicts=[{"order": "AB"|"BA", "winner_branch": "A"|"B", ...}].
    순수 함수 — 동점 시 콘티 사용본(A) 채택은 caller 정책.
    """
    branches = {v.get("winner_branch") for v in verdicts}
    if len(verdicts) == 2 and branches in ({"A"}, {"B"}):
        return verdicts[0]["winner_branch"]
    return "tie"


TIE_POLICY = "tie_keeps_conti_v1"  # 동점/불일치=콘티 사용본(A)


def decision_fingerprint(
    *,
    prompt: str,
    sel_a: Path,
    sel_b: Path,
    judge_model_physical: str,
    prompt_version: str = "1",
) -> str:
    """outer 결정의 내구 지문 (Codex 8e70d4c0 BLOCKING-1).

    prompt + 양 branch sel **내용 해시** + outer 팩 해석값 + 물리 판정
    모델 + tie 정책. 일치=재판정 0콜로 동일 winner 재사용, 어느 하나
    변경=재판정.
    """
    import hashlib
    import json as _json

    payload = {
        "prompt": prompt,
        "sel_a_sha": hashlib.sha256(Path(sel_a).read_bytes()).hexdigest(),
        "sel_b_sha": hashlib.sha256(Path(sel_b).read_bytes()).hexdigest(),
        "outer_pack": resolve_prompt_version(prompt_version),
        "judge_model_physical": judge_model_physical,
        "tie_policy": TIE_POLICY,
    }
    return hashlib.sha256(
        _json.dumps(payload, sort_keys=True).encode("utf-8")
    ).hexdigest()[:16]


def resolve_or_run_outer(
    *,
    prompt: str,
    sel_a: Path,
    sel_b: Path,
    prior_decision: Optional[Dict[str, Any]],
    judge_model_physical: str,
    call_structured_fn: Optional[Callable[..., Dict[str, Any]]] = None,
    project_config: Optional[Dict[str, Any]] = None,
    prompt_version: str = "1",
    opik_metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """durable 결정 resolve — 지문 일치=outer 0콜 재사용, 아니면 판정.

    반환 {fingerprint, winner("A"|"B" — tie 정책 적용 후), outer, judged}.
    판정 실패는 raise — 사전 결정이 없을 때의 fail-open(A 고착)은
    durable 결정 도입으로 불필요, 샷 단위 실패 격리 후 resume 재판정이
    안전측 (Codex B1: 기존 B 승리가 일시 오류로 A 로 뒤집히던 창 제거).
    """
    fp = decision_fingerprint(
        prompt=prompt, sel_a=sel_a, sel_b=sel_b,
        judge_model_physical=judge_model_physical,
        prompt_version=prompt_version,
    )
    if (
        isinstance(prior_decision, dict)
        and prior_decision.get("fingerprint") == fp
        and prior_decision.get("winner") in ("A", "B")
    ):
        return {**prior_decision, "judged": False}
    outer = run_conti_ab_outer_judge(
        prompt=prompt, sel_a=sel_a, sel_b=sel_b,
        call_structured_fn=call_structured_fn,
        project_config=project_config,
        prompt_version=prompt_version,
        opik_metadata=opik_metadata,
    )
    winner = outer["winner"] if outer["winner"] != "tie" else "A"
    return {
        "fingerprint": fp,
        "winner": winner,
        "outer": outer,
        "judged": True,
    }


def run_conti_ab_outer_judge(
    *,
    prompt: str,
    sel_a: Path,
    sel_b: Path,
    call_structured_fn: Optional[Callable[..., Dict[str, Any]]] = None,
    project_config: Optional[Dict[str, Any]] = None,
    prompt_version: str = "1",
    opik_metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """블라인드 2회(순서 뒤집기) outer 판정 → {"winner": "A"|"B"|"tie",
    "verdicts": [...]}. 라벨은 항상 "1"/"2" — 어느 쪽이 콘티 사용본인지
    판정자에게 노출하지 않는다(블라인드).
    """
    from app.modules.pipeline.multiroll_gemini import png_part

    if call_structured_fn is None:
        from app.modules.llm.llm_client import call_structured

        call_structured_fn = call_structured

    resolved = resolve_prompt_version(prompt_version)
    judge_sys = load_prompt(_MODULE, "judge_system", version=resolved)
    judge_schema = load_schema(_MODULE, "judge_schema", None,
                               version=resolved)
    pc = {**(project_config or {}),
          f"{_MODULE}": {"model": AB_JUDGE_MODEL}}

    verdicts = []
    for order, (first, second) in (
        ("AB", (sel_a, sel_b)),
        ("BA", (sel_b, sel_a)),
    ):
        parts = [
            {"type": "text",
             "text": "THE PROMPT (both candidates were generated from"
                     " this):\n" + prompt},
            {"type": "text", "text": "Candidate 1:"},
            png_part(first),
            {"type": "text", "text": "Candidate 2:"},
            png_part(second),
        ]
        res = call_structured_fn(
            _MODULE, judge_sys, parts, judge_schema,
            project_config=pc, schema_name=_MODULE,
            opik_metadata=opik_metadata,
        )
        label = (res.get("winner") or "").strip()
        if order == "AB":
            winner_branch = "A" if label == "1" else "B"
        else:
            winner_branch = "B" if label == "1" else "A"
        verdicts.append({
            "order": order,
            "winner_label": label,
            "winner_branch": winner_branch,
            "reason_ko": res.get("reason_ko") or "",
        })
    return {"winner": decide_winner(verdicts), "verdicts": verdicts}
