"""실제 재실행 카나리아 검사기 — 일부 샷 records 로 새 팩·절 적용 증거 확인.

"제대로 적용되고 있는가"를 LLM 문구 grep 이 아니라 **코드가 조립하는 결정론
상수**와 **기록 자기정합**으로 검사한다 (팩 42 측정 착오의 재발 방지 —
LLM 저작 어휘는 검사 대상이 아니다):

  A. CAMERA 절 존재 — 조립 상수 "- CAMERA: " (camera_frame 팩 스템)
  B. CAMERA 절 본문 == 현행 staging CP 의 camera_direction (결정론 전사 계약
     — 스틸이 stale staging 으로 만들어졌으면 여기서 어긋난다)
  C. LIGHTING & MOOD 절 존재 (lighting 플래그 ON 계약)
  D. identity 역할 절 자기정합 — 캐릭터 참조 라벨("CHARACTER REFERENCE")이
     실제 첨부된 샷 ∧ 플래그 ON ⇔ "CHARACTER REFERENCE ROLE" 절 존재.
     (참조 없는 샷에 절이 있으면 거짓 문장 — 역방향도 위반)
  E. input_fingerprint / _sel mtime — 생성 신선도 표시(정보 제공)

프로덕션 파일은 **읽기 전용**. 종료 코드: 위반 0 = 0, 있으면 1.

사용:
  .venv/bin/python verify_camera_apply.py --project <pid> --episode <eid> \
      [--shots S1sh11,S4sh9] [--identity-flag on|off|auto]
"""
from __future__ import annotations

import argparse
import json
import re
import sys
from datetime import datetime
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

CAMERA_HEAD = "- CAMERA: "            # still_recipe camera_frame 조립 상수
LIGHTING_HEAD = "- LIGHTING & MOOD:"  # lighting_mood 조립 상수
IDENTITY_HEAD = "CHARACTER REFERENCE ROLE"    # identity_ref_role 팩 v16 헤더
CHAR_REF_LABEL_HEAD = "CHARACTER REFERENCE"   # char_label 스템 머리


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--project", required=True)
    ap.add_argument("--episode", required=True)
    ap.add_argument("--shots", default="", help="쉼표 SxshY 목록 — 비면 records 전 샷")
    ap.add_argument(
        "--identity-flag", default="auto", choices=("on", "off", "auto"),
        help="identity 역할 절 기대값 — auto=settings 값")
    args = ap.parse_args()

    from app.core.config import settings

    projects_dir = Path(settings.projects_dir)
    base = projects_dir / args.project / "images" / args.episode / "scene" / "recipe"
    records_path = base / "records.json"
    if not records_path.exists():
        raise SystemExit(f"records 없음: {records_path}")
    records = json.loads(records_path.read_text("utf-8"))

    staging_path = (projects_dir / args.project / "checkpoints" / "episodes"
                    / args.episode / "shot_staging" / "manifest.json")
    staging = json.loads(staging_path.read_text("utf-8"))
    cam_by_key = {
        (s.get("scene_index"), s.get("shot_index")):
            str(s.get("camera_direction") or "").strip()
        for s in staging.get("data", {}).get("shots", [])
    }

    if args.identity_flag == "auto":
        identity_on = bool(getattr(settings, "still_identity_ref_role_enabled", False))
    else:
        identity_on = args.identity_flag == "on"

    if args.shots:
        tags = [t.strip() for t in args.shots.split(",") if t.strip()]
    else:
        tags = sorted(k for k in records if re.fullmatch(r"S\d+sh\d+", k))

    violations: list[str] = []
    rows: list[str] = []
    for tag in tags:
        rec = records.get(tag)
        if not isinstance(rec, dict) or "prompt" not in rec:
            violations.append(f"{tag}: record/prompt 없음 (미생성?)")
            continue
        m = re.fullmatch(r"S(\d+)sh(\d+)", tag)
        if m is None:
            violations.append(f"{tag}: 태그 형식 아님 (SxshY)")
            continue
        si, shi = int(m.group(1)), int(m.group(2))
        prompt = rec["prompt"]
        labels = [str(r.get("label") or "") for r in (rec.get("refs") or [])]
        has_char_ref = any(
            lb.startswith(CHAR_REF_LABEL_HEAD)
            and not lb.startswith(IDENTITY_HEAD) for lb in labels)

        # A. CAMERA 절
        cam_ok = CAMERA_HEAD in prompt
        # B. staging 전사 일치 (staging 이 CAMERA 절을 만들 수 있는 샷만 의미)
        cam_now = cam_by_key.get((si, shi), "")
        transfer_ok = (cam_now in prompt) if (cam_ok and cam_now) else None
        # C. LIGHTING 절
        light_ok = LIGHTING_HEAD in prompt
        # D. identity 절 자기정합
        id_present = IDENTITY_HEAD in prompt
        id_expected = identity_on and has_char_ref
        id_ok = id_present == id_expected

        sel = base / f"{tag}_sel.png"
        sel_m = (datetime.fromtimestamp(sel.stat().st_mtime)
                 .strftime("%m-%d %H:%M") if sel.exists() else "없음")

        def mark(v):  # noqa: ANN001
            return "—" if v is None else ("✓" if v else "✗")

        rows.append(
            f"{tag:>10} | CAM {mark(cam_ok)} | 전사 {mark(transfer_ok)} | "
            f"LIGHT {mark(light_ok)} | ID절 {mark(id_ok)}"
            f"(있음={mark(id_present)},기대={mark(id_expected)}) | "
            f"fp={rec.get('input_fingerprint', '')[:8]} | _sel={sel_m}")

        if not cam_ok:
            violations.append(f"{tag}: CAMERA 절 부재 — staging 절 미조립")
        if transfer_ok is False:
            violations.append(
                f"{tag}: CAMERA 절 본문이 현행 staging camera_direction 과 "
                "불일치 — stale staging 으로 생성된 스틸")
        if not light_ok:
            violations.append(f"{tag}: LIGHTING & MOOD 절 부재")
        if not id_ok:
            violations.append(
                f"{tag}: identity 절 자기정합 위반 — 절 {'있음' if id_present else '없음'}"
                f" vs 기대 {'있음' if id_expected else '없음'}"
                f" (char_ref={'첨부' if has_char_ref else '없음'}, flag={'ON' if identity_on else 'OFF'})")

    print(f"identity flag = {'ON' if identity_on else 'OFF'} | 검사 샷 {len(tags)}")
    print("\n".join(rows))
    if violations:
        print(f"\n★위반 {len(violations)}건:")
        print("\n".join(f"  - {v}" for v in violations))
        raise SystemExit(1)
    print("\n위반 0 — 새 조립 계약이 표본 전 샷에 적용됨")


if __name__ == "__main__":
    main()
