#!/usr/bin/env python3
"""ID 준수 축을 더한다 — 지금 재는 넷은 ID 결함을 못 잡는다.

v38→v39·v41 의 실패는 **validator 반려**(base_id_missing)와 육안 판정으로
나타났지 R1/R3/R4 정규식에는 한 건도 안 걸렸다. ②(ID 규칙 통합)를 건드리기
전에 아래를 먼저 잰다. 이미 Opik 에 쌓인 기록으로 재므로 **돈이 안 든다**.

  A. 회차당 컷 수      — 6 초과 = 재시도(=반려) 발생
  B. 복합 ID 누락      — 카드가 id_and_outlook_required 인데 C##O## 없음
  C. 목록 밖 ID        — allowed_base_entity_ids 에 없는 C##/P##/L## 사용
  D. 이중 묘사         — 같은 인물을 ID 와 보통명사로 두 번
  E. 옷 표현 유무      — 복합 ID 뒤 1~3단어 의류
"""
import json
import re
import sys
import datetime as dt
from collections import defaultdict

sys.path.insert(0, "/Users/manta/Documents/Projects/TheRoad-I1/scratchpad")
from _opik_env import opik_target  # noqa: E402
from tools.opik_prompt_audit.audit.fetch import fetch_spans  # noqa: E402

EDITION = {29731: "v46", 31965: "v45", 37817: "v44", 37368: "v43"}
ID_ANY = re.compile(r"\b([CPL]\d{2})(O\d{2})?\b")

BASE, WS, PROJ = opik_target()
sd = sorted((x for x in fetch_spans(BASE, WS, PROJ, "2026-08-24T16:00:00",
                                    "2026-08-26T00:00:00")
             if "op:scene_detail" in (x.get("tags") or [])),
            key=lambda x: x["start_time"])

by = defaultdict(list)
for x in sd:
    inp = x.get("input")
    msgs = inp if isinstance(inp, list) else (inp or {}).get("messages") or []
    syslen, card = 0, None
    for m in msgs:
        c = m.get("content")
        if isinstance(c, list):
            c = "\n".join(p.get("text", "") for p in c if isinstance(p, dict))
        c = c or ""
        if m.get("role") == "system":
            syslen = len(c)
        elif m.get("role") == "user" and "[RenderPromptCard v1]" in c:
            mm = re.search(r"\[RenderPromptCard v1\]\n(\{.*?\})\n", c, re.S)
            if mm:
                try:
                    card = json.loads(mm.group(1))
                except Exception:
                    pass
    if card is None:
        continue
    try:
        out = json.loads(x["output"]["choices"][0]["message"]["content"])
    except Exception:
        continue
    ip = card.get("id_policy") or {}
    allowed = set(ip.get("allowed_base_entity_ids") or [])
    pairs = {f"{p['character_id']}{p['outlook_id']}"
             for p in (ip.get("allowed_outlook_pairs") or [])}
    srp = {p.get("subject_id") or p.get("character_id"): p.get("policy")
           for p in (ip.get("subject_reference_policy") or [])}
    sk = card.get("shot_key") or {}
    shot = f"S{sk.get('scene_index')}sh{sk.get('shot_index')}"
    for v in (out.get("t2i_variations") or []):
        p = v.get("t2i_prompt") or ""
        found = ID_ANY.findall(p)
        comp = {b + o for b, o in found if o}
        bare = {b for b, o in found if not o}
        chars = {b for b, o in found if b.startswith("C")}
        # 기본값이 id_and_outlook_required 인 인물이 composite 없이 쓰였나
        need_comp = {c for c in chars
                     if srp.get(c, "id_and_outlook_required") == "id_and_outlook_required"}
        missing_comp = {c for c in need_comp if not any(k.startswith(c) for k in comp)}
        outside = {b for b, o in found if b not in allowed}
        bad_pair = {k for k in comp if k not in pairs}
        by[syslen].append({
            "ts": dt.datetime.fromisoformat(x["start_time"].replace("Z", "+00:00")),
            "shot": shot, "missing_comp": missing_comp,
            "outside": outside, "bad_pair": bad_pair,
            "ncomp": len(comp), "nbare": len(bare)})

print("ID 준수 축 — 이미 쌓인 기록으로 (추가 비용 0)\n")
for syslen in sorted(by, reverse=True):
    rows = [r for r in by[syslen]
            if r["shot"].startswith("S") and r["shot"][1].isdigit()
            and int(r["shot"][1:r["shot"].index("s")]) <= 3]
    if not rows:
        continue
    n = len(rows)
    name = EDITION.get(syslen, f"?{syslen}")
    # 회차 = 6컷씩
    runs = [rows[i:i + 6] for i in range(0, n, 6)]
    shots_per_run = [len({r["shot"] for r in g}) for g in runs]
    print(f"── {name}  system {syslen:,}자  ·  {n}컷 ({len(runs)}회)")
    print(f"   A 회차별 고유 샷 수 : {shots_per_run}  (6 이면 재시도 없음)")
    print(f"   B 복합 ID 누락      : {sum(bool(r['missing_comp']) for r in rows)}/{n}"
          f"  {sorted({c for r in rows for c in r['missing_comp']}) or ''}")
    print(f"   C 목록 밖 ID        : {sum(bool(r['outside']) for r in rows)}/{n}"
          f"  {sorted({c for r in rows for c in r['outside']}) or ''}")
    print(f"   D 미허용 아웃룩 짝  : {sum(bool(r['bad_pair']) for r in rows)}/{n}"
          f"  {sorted({c for r in rows for c in r['bad_pair']}) or ''}")
    print(f"   E 컷당 복합 ID 평균 : {sum(r['ncomp'] for r in rows)/n:.2f}"
          f"  · bare 평균 {sum(r['nbare'] for r in rows)/n:.2f}\n")
