"""참조가 그림을 낫게 하나 — **눈가림 A/B**. ★`--live` 는 유료(이미지 생성).

## 무엇을 하나

고른 참조가 있는 대상마다 **같은 프롬프트**로 두 벌을 굽는다 —

    A arm   참조 **없이**      (baseline)
    B arm   참조 **넣어서**    (reference)

★**seed 가 없다.** 우리가 쓰는 이미지 API 는 전부 seed 옵션이 없어서 「같은
seed 로 맞춘다」가 불가능하다. 그래서 arm 당 **여러 장**을 굽고 **순서를 가려**
사람이 본다.

## 자동은 판을 차릴 뿐이다

    자동   같은 프롬프트인가 · arm 신원 · 순서 은닉 · 기록 · 비용
    사람만 **어느 쪽이 나은가** · 고증이 맞는가 · 품질

★VLM 에게 좋고 나쁨을 안 묻는다. 점수·critique·다수결을 근거로 안 쓴다.

    python tools/grounding_audit/ref_ab.py --dry  <j_ref.json> <outdir>
    python tools/grounding_audit/ref_ab.py --live <j_ref.json> <outdir>
"""
from __future__ import annotations

import hashlib
import json
import random
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional

ROOT = Path(__file__).resolve().parents[2]
SERVE_ROOT = ROOT.parent
sys.path.insert(0, str(ROOT))

#: arm 당 몇 장. ★seed 가 없으므로 한 장으로는 **판마다 다른 것**과
#:  **arm 차이**를 못 가른다.
REPEATS = 2

#: ★**손으로 적은 생성 상한.** 대상 × arm 2 × 반복. 넘으면 안 굽고 선다.
#:
#:  ★수를 **실측 뒤에 손으로** 고쳤다 (2026-08-31). 배선 셋을 고치자 참조를
#:   구한 대상이 10 → **14** 로 늘어 56장이 됐고 48에 걸려 섰다 — 게이트가
#:   제 일을 했다. 64 는 실측 56 + 여유 8 이다. **계획에서 계산해 넓힌 것이
#:   아니라** 실제 수를 보고 사람 권한 아래 손으로 적었다.
MAX_IMAGES = 64

ARM_BASELINE = "baseline_no_reference"
ARM_REFERENCE = "with_reference"


class ApprovedScopeMismatch(RuntimeError):
    """계획이 승인 수와 다르다. ★한 장도 안 굽고 선다."""


def plan_from(doc: Dict[str, Any]) -> List[Dict[str, Any]]:
    """고른 참조가 있는 대상만. ★없는 것은 비교할 짝이 없다."""
    out = []
    for rec in doc.get("records") or ():
        if rec.get("status") != "selected" or not rec.get("chosen_path"):
            continue
        t = rec.get("target") or {}
        brief = str(t.get("visual_brief") or "").strip()
        if not brief:
            continue
        out.append({
            "subject_id": t.get("final_id") or t.get("local_id"),
            "owner_type": t.get("owner_type"),
            "surface_form": t.get("surface_form"),
            # ★프롬프트는 **모델이 적은 겉모습**이다. 여기서 문장을 새로
            #  지으면 그것이 하드 프롬프트다.
            "prompt": brief,
            "reference_path": rec["chosen_path"],
        })
    return sorted(out, key=lambda x: str(x["subject_id"]))


def _blind_key(subject_id: str, arm: str, n: int) -> str:
    """가린 이름. ★arm 이 파일 이름에서 안 보이게."""
    h = hashlib.sha256(f"{subject_id}|{arm}|{n}".encode()).hexdigest()
    return h[:10]


def main() -> int:
    if len(sys.argv) < 4 or sys.argv[1] not in ("--dry", "--live"):
        print(__doc__)
        return 2
    mode = sys.argv[1]
    doc = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8"))
    # ★★**절대로 푼다.** 상대 경로를 그대로 두면 `relative_to` 가 터진다 —
    #  같은 부류를 참조 획득에서도 냈다 (2026-08-31).
    outdir = Path(sys.argv[3]).resolve()

    plan = plan_from(doc)
    total = len(plan) * 2 * REPEATS
    print(f"■ A/B {mode[2:]} — 대상 {len(plan)}개 × arm 2 × 반복 {REPEATS} "
          f"= **{total}장** (상한 {MAX_IMAGES})")
    if total > MAX_IMAGES:
        raise ApprovedScopeMismatch(
            f"굽는 수가 {total} 인데 승인 상한은 {MAX_IMAGES} 이다 — 사람이 "
            "다시 정해야 한다. 한 장도 안 굽고 선다")
    if mode == "--dry":
        for p in plan:
            print(f"   {p['owner_type']:14} {str(p['surface_form'])[:18]:20} "
                  f"참조 {p['reference_path'].split('/')[-1]}")
        return 0

    from app.modules.llm.gemini_image_client import GeminiImageClient

    client = GeminiImageClient()
    outdir.mkdir(parents=True, exist_ok=True)
    rows: List[Dict[str, Any]] = []
    made = 0
    for p in plan:
        ref_bytes = (SERVE_ROOT / p["reference_path"]).read_bytes()
        for arm in (ARM_BASELINE, ARM_REFERENCE):
            for n in range(1, REPEATS + 1):
                key = _blind_key(str(p["subject_id"]), arm, n)
                dest = outdir / f"{key}.png"
                if dest.exists():
                    # ★이미 구운 것은 **다시 안 산다**
                    rows.append({**p, "arm": arm, "n": n, "key": key,
                                 "path": str(dest.relative_to(SERVE_ROOT)),
                                 "reused": True})
                    continue
                try:
                    png, ms = client.generate_image(
                        p["prompt"],
                        reference_images=([ref_bytes]
                                          if arm == ARM_REFERENCE else None),
                        aspect_ratio="4:3")
                except Exception as exc:            # noqa: BLE001
                    print(f"   ★{p['subject_id']} {arm} #{n} 실패: {exc}")
                    rows.append({**p, "arm": arm, "n": n, "key": key,
                                 "path": "", "error": str(exc)})
                    continue
                dest.write_bytes(png)
                made += 1
                rows.append({**p, "arm": arm, "n": n, "key": key,
                             "path": str(dest.relative_to(SERVE_ROOT)),
                             "ms": ms})
        print(f"   {p['owner_type']:14} {str(p['surface_form'])[:18]:20} 끝")

    # ★★**순서를 가린다.** 화면은 이 섞인 차례로만 보여 주고, 어느 것이 어느
    #  arm 인지는 **따로** 적는다 — 사람이 보기 전에 알면 판이 기운다.
    rng = random.Random(0xA11CE)
    for p in plan:
        mine = [r for r in rows if r["subject_id"] == p["subject_id"]
                and r.get("path")]
        rng.shuffle(mine)
        for i, r in enumerate(mine, 1):
            r["slot"] = i

    rec = {"mode": mode[2:], "repeats": REPEATS, "planned": total,
           "generated": made, "rows": rows,
           "note": ("★seed 가 없다 — 같은 seed 로 못 맞춘다. arm 당 여러 장을 "
                    "굽고 **순서를 가려** 사람이 본다. VLM 에게 좋고 나쁨을 "
                    "묻지 않는다.")}
    out = outdir / "ab.json"
    out.write_text(json.dumps(rec, ensure_ascii=False, indent=1),
                   encoding="utf-8")
    print(f"■ 구운 것 {made}장 (되쓴 것 {sum(1 for r in rows if r.get('reused'))}) "
          f"· 실패 {sum(1 for r in rows if r.get('error'))}")
    print(f"  적었다: {out}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
