"""GROUNDING-V2 §4b — batch 실험용 **12 subject manifest** 를 잠근다.

★유료 호출 **전에** 고정한다 (Codex). 무엇을 언제 정했는지가 안 남으면,
결과를 보고 표본을 고른 것과 구분되지 않는다.

★subject 는 **프로덕션 함수**(`build_subjects_from_saved_episode` →
`grounding_carry.build_subjects`)로 만든다. 여기서 따로 조립하면 도구가
프로덕션과 다른 입력을 보내게 된다 — 실제로 그랬던 판이 있다.

무료다. 바깥 호출을 하지 않는다.

    python -m tools.prompt_measure.build_claims_search_manifest \
        --projects-root projects --out backend/tests/fixtures/grounding/claims_search_manifest.json
"""
from __future__ import annotations

import argparse
import hashlib
import json
import sys
from pathlib import Path

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

from app.modules.pipeline.grounding_shadow import (  # noqa: E402
    build_subjects_from_saved_episode)

#: ★**held-out 6개는 안 쓴다.** 그것은 §2-3c 판정용으로 잠근 것이고, 검색
#:  결과를 보고 무엇이든 만지면 held-out 성질이 사라진다.
HELDOUT_IS_OFF_LIMITS = True

#: 대조군에서 가져오는 것 — 중복 대상(두 에피소드의 같은 회수권 뭉치) 하나 제거.
#: ★`쇠사슬로 묶인 기계식 요금통` 과 `쇠사슬에 묶인 투명 요금통` 이 이미
#:  **표면형이 비슷한 두 대상**이라, 결속 충돌을 실제로 압박한다 (Codex 요청).
FROM_CONTROLS = [
    ("97375a4b", "P01"),   # 쇠사슬로 묶인 기계식 요금통
    ("97375a4b", "P03"),   # 고무줄로 묶인 낡은 종이 회수권 뭉치
    ("0aea12c2", "P01"),   # 쇠사슬에 묶인 투명 요금통   ← 위와 표면형 충돌
    ("fb7a883f", "P01"),   # 낡은 자전거 체인
    ("fb7a883f", "P03"),   # 휴대용 손전등
    ("fb7a883f", "L01"),   # 좁은 자전거 정비소 내부
]
CONTROLS_PROJECT = "da049582"


def _episode_dir(root: Path, project_prefix: str, episode_prefix: str) -> Path:
    for d in sorted(root.glob(
            f"{project_prefix}*/checkpoints/episodes/{episode_prefix}*")):
        if (d / "entity_merge" / "manifest.json").exists():
            return d
    raise SystemExit(f"에피소드를 못 찾았다: {project_prefix}/{episode_prefix}")


def _rules(ep: Path) -> dict:
    p = ep / "visual_world_rules" / "manifest.json"
    return json.loads(p.read_text(encoding="utf-8"))["data"]


def collect(root: Path) -> list[dict]:
    """대조군 6개를 **프로덕션 경로로** 만든다."""
    out: list[dict] = []
    by_ep: dict[str, list[str]] = {}
    for ep_prefix, sid in FROM_CONTROLS:
        by_ep.setdefault(ep_prefix, []).append(sid)
    for ep_prefix, sids in by_ep.items():
        ep = _episode_dir(root, CONTROLS_PROJECT, ep_prefix)
        pid, eid = ep.parents[2].name, ep.name
        built = build_subjects_from_saved_episode(
            ep, project_id=pid, episode_id=eid)
        rules = _rules(ep)
        idx = {(s.get("provenance") or {}).get("short_id"): s
               for s in built["subjects"]}
        for sid in sids:
            s = idx.get(sid)
            if s is None:
                # ★못 찾은 것을 조용히 건너뛰지 않는다 — 표본이 줄어든 채
                #  「12개로 쟀다」가 된다.
                raise SystemExit(f"{ep_prefix}/{sid} 가 후보에 없다")
            out.append({
                "research_subject_id": s["research_subject_id"],
                "surface_form": s.get("surface_form") or "",
                "source_quote": s.get("source_quote") or "",
                "quote_source": s.get("quote_source") or "",
                "owner_type": s.get("owner_type") or "",
                "project_id": pid, "episode_id": eid,
                "short_id": sid, "coord": f"{ep_prefix}/{sid}",
                "era": str(rules.get("era") or ""),
                "region": str(rules.get("region") or ""),
                "origin": "controls",
            })
    return out


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--projects-root", type=Path, default=Path("projects"))
    ap.add_argument("--out", type=Path, required=True)
    args = ap.parse_args()

    rows = collect(args.projects_root)
    # ★**선정 순서를 고정**한다. 순서가 흔들리면 batch 경계가 판마다 달라져
    #  같은 실험이 아니게 된다.
    rows.sort(key=lambda r: r["research_subject_id"])
    body = json.dumps(rows, ensure_ascii=False, sort_keys=True)
    doc = {
        "contract": "grounding-v2 §4b batch 실험 표본",
        "why": ("유료 호출 전에 잠근다. 결과를 보고 표본을 고른 것과 구분되려면 "
                "무엇을 언제 정했는지가 남아야 한다."),
        "heldout_excluded": ("§2-3c 판정용 6개는 안 쓴다 — 검색 결과를 보고 "
                             "무엇이든 만지면 held-out 성질이 사라진다"),
        "count": len(rows),
        "subjects": rows,
        "content_hash": hashlib.sha256(body.encode()).hexdigest()[:16],
    }
    args.out.parent.mkdir(parents=True, exist_ok=True)
    args.out.write_text(json.dumps(doc, ensure_ascii=False, indent=2) + "\n",
                        encoding="utf-8")
    print(f"{len(rows)}개 · content_hash={doc['content_hash']} → {args.out}")
    for r in rows:
        print(f"  {r['coord']:16} {r['owner_type']:10} {r['surface_form']}")
    return 0


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