"""이중 판정이 실제로 무엇을 바꿨는가 — 이중 범위를 정하기 위한 실측.

묻는 것은 하나다: **Sol 을 부를 값어치가 어디에 있는가.**
Opus 단독으로 이미 맞히는 샷에서 Sol 을 또 부르면 콜만 두 배가 된다.
그래서 다음 셋을 나눠 센다.

  ① Sol 이 판정을 뒤집었는가 — 이중 승자 ≠ Opus 단독 승자
  ② 뒤집힌 샷에서 Opus 는 어떤 상태였는가 — 위반을 하나도 못 봤는가,
     점수 차가 근소했는가. 이것이 "언제 Sol 을 부를지"의 조건이 된다.
  ③ Sol 만 잡은 하드 위반이 몇 건이고 어떤 성질인가.

사용: .venv/bin/python analyze_dual.py <dual.json> [<opus_single.json>]
"""
from __future__ import annotations

import json
import re
import sys
from collections import Counter
from pathlib import Path
from typing import Any, Dict

OPUS, SOL = "claude-opus", "gpt"


def norm_scores(res: Dict[str, Any], labels) -> Dict[str, float]:
    rd = res.get("readings") or []
    top = max((r.get("score") or 0) for r in rd) if rd else 0
    return {r["label"]: (r.get("score") or 0) / (top or 1) for r in rd
            if r["label"] in labels}


def main() -> None:
    dual = json.loads(Path(sys.argv[1]).read_text("utf-8"))
    single = (json.loads(Path(sys.argv[2]).read_text("utf-8"))
              if len(sys.argv) > 2 else {})

    n = flip = both_ok = 0
    flip_stems, flip_cond = [], Counter()
    sol_only, opus_only = 0, 0
    sol_only_examples = []
    margin_when_flip, margin_when_same = [], []
    prior_change_dual = prior_change_opus = 0

    for stem, v in dual.items():
        sel = v.get("select")
        if not sel or "per_model" not in sel:
            continue
        pm = sel["per_model"]
        if OPUS not in pm or "readings" not in pm[OPUS]:
            continue
        n += 1
        labels = list(sel["normalized"])
        ow = pm[OPUS]["winner"]
        dw = sel["winner"]
        prior = v.get("prior_selected_roll")
        if dw != prior:
            prior_change_dual += 1
        if ow != prior:
            prior_change_opus += 1

        # ③ 심판별 하드 위반 귀속
        for r in pm[OPUS].get("readings") or []:
            if r.get("hard_violations") and not any(
                x.get("hard_violations")
                for x in (pm.get(SOL, {}).get("readings") or [])
                if x["label"] == r["label"]
            ):
                opus_only += 1
        for r in (pm.get(SOL, {}).get("readings") or []):
            same = [x for x in (pm[OPUS].get("readings") or [])
                    if x["label"] == r["label"]]
            if r.get("hard_violations") and same and not same[0].get(
                    "hard_violations"):
                sol_only += 1
                if len(sol_only_examples) < 12:
                    sol_only_examples.append(
                        f"{stem}[{r['label']}] {r['hard_violations'][0][:78]}")

        # ①② 뒤집힘과 그때 Opus 의 상태
        ns = norm_scores(pm[OPUS], labels)
        srt = sorted(ns.values(), reverse=True)
        margin = (srt[0] - srt[1]) if len(srt) > 1 else 1.0
        if dw != ow:
            flip += 1
            flip_stems.append(stem)
            margin_when_flip.append(margin)
            win_r = [r for r in (pm[OPUS].get("readings") or [])
                     if r["label"] == ow]
            no_vio = not (win_r and win_r[0].get("hard_violations"))
            flip_cond["Opus 가 선정본에 위반 0" if no_vio
                      else "Opus 도 위반을 봤음"] += 1
            flip_cond["Opus 점수차 근소(<0.30)" if margin < 0.30
                      else "Opus 점수차 큼(>=0.30)"] += 1
        else:
            both_ok += 1
            margin_when_same.append(margin)

    def med(a):
        a = sorted(a)
        return a[len(a) // 2] if a else 0.0

    print(f"=== 이중 판정 실측 (샷 {n}) ===\n")
    print(f"① Sol 이 판정을 뒤집은 샷 : {flip} ({100*flip/max(n,1):.1f}%)")
    print(f"   두 심판 승자 일치       : {both_ok} ({100*both_ok/max(n,1):.1f}%)")
    print(f"\n② 뒤집힌 샷에서 Opus 의 상태")
    for k, c in flip_cond.most_common():
        print(f"   {k:28s} {c}")
    print(f"   Opus 점수차 중앙값 — 뒤집힘 {med(margin_when_flip):.3f} / "
          f"일치 {med(margin_when_same):.3f}")
    print(f"\n③ 한쪽만 잡은 하드 위반")
    print(f"   Sol 만 잡음  : {sol_only}")
    print(f"   Opus 만 잡음 : {opus_only}")
    for e in sol_only_examples:
        print(f"     · {e}")
    print(f"\n④ 기존(Gemini) 선정과 달라진 샷")
    print(f"   이중      : {prior_change_dual} ({100*prior_change_dual/max(n,1):.1f}%)")
    print(f"   Opus 단독 : {prior_change_opus} ({100*prior_change_opus/max(n,1):.1f}%)")
    if flip_stems:
        print(f"\n뒤집힌 샷: {', '.join(sorted(flip_stems, key=lambda s: tuple(int(x) for x in re.findall(r'\\d+', s))))}")

    # 조건부 이중의 절감 추정 — "Opus 가 위반 0 이고 점수차가 큰" 샷을 건너뛴다면
    skip = sum(1 for stem, v in dual.items()
               if (sel := v.get("select")) and "per_model" in sel
               and OPUS in sel["per_model"]
               and "readings" in sel["per_model"][OPUS]
               and not any(r.get("hard_violations")
                           for r in sel["per_model"][OPUS]["readings"]
                           if r["label"] == sel["per_model"][OPUS]["winner"])
               and (lambda s: (s[0] - s[1] if len(s) > 1 else 1.0) >= 0.30)(
                   sorted(norm_scores(sel["per_model"][OPUS],
                                      list(sel["normalized"])).values(),
                          reverse=True)))
    lost = sum(1 for stem in flip_stems
               if (sel := dual[stem]["select"])
               and not any(r.get("hard_violations")
                           for r in sel["per_model"][OPUS]["readings"]
                           if r["label"] == sel["per_model"][OPUS]["winner"])
               and (lambda s: (s[0] - s[1] if len(s) > 1 else 1.0) >= 0.30)(
                   sorted(norm_scores(sel["per_model"][OPUS],
                                      list(sel["normalized"])).values(),
                          reverse=True)))
    print(f"\n⑤ 조건부 이중 시뮬레이션 — 'Opus 위반 0 · 점수차 ≥0.30' 이면 Sol 생략")
    print(f"   Sol 호출 생략 가능 : {skip}/{n} ({100*skip/max(n,1):.1f}%)")
    print(f"   그때 놓치는 뒤집힘 : {lost}/{flip}"
          + (f" ({100*lost/flip:.1f}%)" if flip else ""))


if __name__ == "__main__":
    main()
