"""드리프트 수정으로 인물 배정이 바뀐 표본 샷을 다시 그린다.

수정 전 이미지는 `artifact/20260807_scene_director_drift/before/` 에 이미
복사해 두었다(원본은 자리에 그대로 — 옮기지 않았다).

샷마다 두 단계다.
  ① `scene-detail/redo-shot` — 새 VE·새 아웃룩으로 T2I 프롬프트를 다시 만든다.
     여기서 실패하면 그 샷은 **건너뛴다**. 실패는 계약이 지켜졌다는 뜻이라
     조용히 넘기지 않고 이유를 남긴다(실측: 아웃룩 누락 시
     `id_and_outlook_required` 로 거부됐다).
  ② `stills/{id}/generate-image` — 이미지 생성.

    .venv/bin/python regen_drift_fixed_shots.py [--dry]
"""
from __future__ import annotations

import argparse
import json
import sys
import time
import urllib.error
import urllib.request

BASE = "http://localhost:8000/api/v1"
PROJ = "e716bafb-24bb-42b7-aea0-fdb383844ee8"
EPI = "d6a9aa85-b75e-400c-980c-4ee7e876a15b"

# (scene, shot, still_id, 옛 인물 → 새 인물)
TARGETS = [
    (11, 3, "4180bb21-f868-4e7e-a723-f07e4727138d", "(없음) → 정인우"),
    (15, 5, "761355ad-2242-4634-8312-661f7791f1e2", "(없음) → 고산들"),
    (88, 6, "646744a5-f7b9-444d-acb0-cb7673a55296", "육안 지목 + VE 변화"),
    (7, 1, "a3d7f47f-3ddb-41e6-b565-0d8f910dc864", "정인우 → 강민숙+정인우"),
    (8, 1, "cedfa9b6-0c88-472a-a034-ff5f644250c4", "정인우 → 김지후+정인우"),
    (9, 6, "f31e84a8-7588-41d6-8c92-303de54ac7b7", "(없음) → 강민숙+고산들"),
    (12, 10, "2a2293f8-1937-45ce-836b-1e209ecc6e52", "김지후 → 고산들+김지후"),
    (17, 6, "b82c5158-a543-4695-a225-41c5fd02efa1", "(없음) → 고산들+김지후"),
]


def login() -> str:
    req = urllib.request.Request(
        f"{BASE}/auth/login",
        data=json.dumps({"username": "admin", "password": "admin123"}).encode(),
        headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req) as r:
        return r.headers.get("Set-Cookie", "").split(";")[0]


def post(cookie: str, path: str, timeout: int = 900, body: dict | None = None) -> dict:
    # 본문이 필수인 엔드포인트가 있다 — 빈 POST 로 보내면 422 `Field required`.
    # `GenerateSingleRequest` 는 필드가 전부 선택이지만 본문 자체는 필수다.
    data = json.dumps(body).encode() if body is not None else b""
    headers = {"Cookie": cookie}
    if body is not None:
        headers["Content-Type"] = "application/json"
    req = urllib.request.Request(f"{BASE}{path}", data=data, headers=headers)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            return json.loads(r.read() or b"{}")
    except urllib.error.HTTPError as e:
        body = e.read().decode()[:400]
        try:
            return {"_http_error": json.loads(body)}
        except json.JSONDecodeError:
            return {"_http_error": body}
    except Exception as exc:  # noqa: BLE001
        return {"_error": repr(exc)[:300]}


def _err_text(err) -> str:
    """오류 원문을 잃지 않는다 — 기대한 모양이 아니면 통째로 남긴다.

    2026-08-07: `err["error"]["message"]` 만 꺼내다가 404 본문
    `{"detail":"Not Found"}` 을 만나 8건 전부 `None` 으로 보고했다. 사유를
    삼키면 경로 오타 하나를 찾는 데 재실행이 한 번 더 든다.
    """
    if isinstance(err, dict):
        e = err.get("error")
        if isinstance(e, dict) and e.get("message"):
            return str(e["message"])
        if err.get("detail"):
            return f"detail={err['detail']}"
        return json.dumps(err, ensure_ascii=False)[:300]
    return str(err)[:300]


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--dry", action="store_true")
    ap.add_argument("--images-only", action="store_true",
                    help="scene_detail 은 건너뛰고 이미지 생성만")
    a = ap.parse_args()

    if a.dry:
        for si, shi, sid, note in TARGETS:
            print(f"  S{si}sh{shi:<3} {sid}  {note}")
        return

    cookie = login()
    ok_detail, ok_image, failed = [], [], []

    for si, shi, still_id, note in TARGETS:
        tag = f"S{si}sh{shi}"
        t0 = time.time()
        r = {} if a.images_only else post(cookie, f"/projects/{PROJ}/episodes/{EPI}/steps/"
                         f"scene-detail/redo-shot?scene_index={si}&shot_index={shi}&mode=force")
        err = r.get("_http_error") or r.get("_error")
        if err:
            msg = _err_text(err)
            print(f"[{tag}] scene_detail 거부 — {str(msg)[:150]}", flush=True)
            failed.append((tag, "scene_detail", str(msg)[:200]))
            continue
        ok_detail.append(tag)
        if not a.images_only:
            print(f"[{tag}] scene_detail 재생성 OK ({time.time()-t0:.0f}s) · {note}", flush=True)

        t1 = time.time()
        # ★images 라우터 prefix 는 /api/v1/projects/{pid} 뿐 — episodes 구간이 없다.
        #  넣었다가 8샷 전부 404 를 맞았다(2026-08-07).
        g = post(cookie, f"/projects/{PROJ}/stills/{still_id}/generate-image", body={})
        gerr = g.get("_http_error") or g.get("_error")
        if gerr:
            msg = _err_text(gerr)
            print(f"[{tag}] 이미지 생성 실패 — {str(msg)[:150]}", flush=True)
            failed.append((tag, "image", str(msg)[:200]))
            continue
        ok_image.append((tag, g.get("id"), g.get("file_path") or g.get("path")))
        print(f"[{tag}] 이미지 생성 OK ({time.time()-t1:.0f}s) id={g.get('id')}", flush=True)

    print(f"\nscene_detail 성공 {len(ok_detail)}/{len(TARGETS)} · "
          f"이미지 성공 {len(ok_image)}/{len(TARGETS)}")
    for tag, stage, msg in failed:
        print(f"  실패 {tag} [{stage}] {msg}")
    out = {"ok_detail": ok_detail,
           "ok_image": [{"stem": t, "image_id": i, "path": p} for t, i, p in ok_image],
           "failed": [{"stem": t, "stage": s, "message": m} for t, s, m in failed]}
    print("\nJSON=" + json.dumps(out, ensure_ascii=False))


if __name__ == "__main__":
    main()
