"""아웃룩 phase2·phase3 를 다시 돌리고, 표적 아닌 하류 체크포인트를 되돌린다.

## ★재실행에는 `force` 가 필요하다 (2026-08-07 실측으로 정정)

처음엔 "manifest 를 옆으로 치우고 resume" 으로 하려 했는데 **안 돈다**.
StepRunner 에 자동 복원이 있어서 manifest 가 없으면 실행하는 대신 보관본에서
되살린다:

    Step outlook_phase2: manifest.json missing/empty/corrupt.
    Auto-restored from archive manifest_20260807_074742.json

그래서 두 번을 헛돌렸다(드리프트 시절 8/04 데이터가 그대로 되살아났다).
기존 결과가 있는 스텝을 다시 돌리는 길은 `force` 뿐이다.

## force 의 대가와 되돌리기

`force` 는 하류를 `delete_cp=True` 로 무효화한다. 삭제 직전 `_archive_manifest`
가 `manifest_<stamp>.json` 을 남기므로 내용은 살아 있다. 이 스크립트가
표적(RERUN) 외 전부를 **가장 최근 보관본**에서 되돌린다 — 추가 전용이라
새로 생긴 결과를 덮어쓰지 않는다.

    .venv/bin/python run_outlook_chain.py
"""
from __future__ import annotations

import json
import shutil
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

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

from app.core.config import settings  # noqa: E402

BASE = "http://localhost:8000/api/v1"
PROJ = "e716bafb-24bb-42b7-aea0-fdb383844ee8"
EPI = "d6a9aa85-b75e-400c-980c-4ee7e876a15b"
CP = Path(settings.projects_dir) / PROJ / "checkpoints" / "episodes" / EPI
RERUN = ["outlook_phase2", "outlook_phase3"]


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) -> dict:
    req = urllib.request.Request(f"{BASE}{path}", data=b"", headers={"Cookie": cookie})
    try:
        with urllib.request.urlopen(req, timeout=600) as r:
            return json.loads(r.read())
    except urllib.error.HTTPError as e:
        return {"ok": False, "error": e.read().decode()[:300]}


def wait_fresh(step: str, after: float, timeout_s: int = 2400) -> bool:
    """`after` 이후에 **새로 쓰인** manifest 만 성공으로 본다.

    존재만 보면 자동 복원된 옛 파일을 완료로 오독한다 — 실측된 함정이다.
    """
    t0 = time.time()
    p = CP / step / "manifest.json"
    while time.time() - t0 < timeout_s:
        if p.exists() and p.stat().st_mtime > after:
            return True
        time.sleep(20)
    return False


def newest_stamp() -> str:
    stamps = {a.name[len("manifest_"):-len(".json")]
              for d in CP.iterdir() if d.is_dir()
              for a in d.glob("manifest_2*.json")}
    return max(stamps) if stamps else ""


def main() -> None:
    cookie = login()

    for step in RERUN:
        t0 = time.time()
        print(f"[run ] {step} (force — 기존 결과가 있으면 force 만 재실행한다)", flush=True)
        r = post(cookie, f"/projects/{PROJ}/episodes/{EPI}/steps/{step}?mode=force")
        if not r.get("ok") and "already_running" not in str(r.get("error", "")):
            sys.exit(f"{step} 시작 실패: {r}")
        if not wait_fresh(step, t0):
            sys.exit(f"{step} 타임아웃 또는 새 결과 없음 — 로그를 봐라")
        d = json.loads((CP / step / "manifest.json").read_text("utf-8"))
        print(f"[done] {step} · updated={d.get('updated_at','?')[:19]}", flush=True)

    stamp = newest_stamp()
    restored = []
    for d in sorted(p for p in CP.iterdir() if p.is_dir()):
        if d.name in RERUN:
            continue
        live, arch = d / "manifest.json", d / f"manifest_{stamp}.json"
        if live.exists() or not arch.exists():
            continue
        shutil.copy2(arch, live)          # 추가 전용
        restored.append(d.name)
    print(f"\n보관본 {stamp} 에서 되돌림 {len(restored)}개: {restored}", flush=True)
    missing = [p.name for p in CP.iterdir()
               if p.is_dir() and not (p / "manifest.json").exists()]
    print(f"아직 manifest 없는 스텝: {missing or '없음'}")

    # 검증 — 씬 키가 세그먼트 도메인인가, 그리고 문제 샷이 덮이는가
    segs = json.loads((CP / "scene_save" / "manifest.json").read_text("utf-8"))["data"]["segments"]
    seg_idx = {s["scene_index"] for s in segs}
    d3 = json.loads((CP / "outlook_phase3" / "manifest.json").read_text("utf-8"))["data"]
    rows = d3.get("scene_assignments") or []
    idx = [r.get("scene_index") for r in rows]
    print(f"\n씬 행 {len(rows)} · 범위 {min(idx)}~{max(idx)} · "
          f"세그먼트 밖 {sorted(set(idx) - seg_idx) or '없음'}")
    for r in rows:
        if r.get("scene_index") == 11:
            ids = [a.get("character_id") for a in r.get("assignments") or []]
            print(f"씬 11 아웃룩 배정: {ids} → C03(정인우) 포함: {'C03' in ids}")
            break


if __name__ == "__main__":
    main()
