"""force 무효화가 지운 하류 체크포인트를 보관본에서 되돌린다.

## 왜 필요한가 (2026-08-07 실측)

`scene_director` 를 `mode=force` 로 재실행하자 StepRunner 가 하류 55개 스텝을
`delete_cp=True` 로 무효화했다 — 46개 스텝의 `manifest.json` 이 사라졌다.
승인된 범위는 scene_director + shot_director 두 개뿐이었다.

다행히 `step_runner._invalidate_downstream` 은 삭제 직전에 `_archive_manifest`
로 `manifest_<YYYYMMDD_HHMMSS>.json` 을 남긴다(step_runner.py:491-493). 그래서
내용은 온전히 살아 있고, 이 스크립트가 그 보관본을 제자리로 되돌린다.

## 가드 (전부 코드에 있다 — 사람 주의력에 맡기지 않는다)

  · **추가 전용** — `manifest.json` 이 이미 있는 스텝은 건드리지 않는다.
    되돌리기가 새 결과를 덮어쓰는 일이 절대 없다.
  · **표적 제외** — 일부러 재실행한 스텝(`--exclude`)은 되돌리지 않는다.
  · **보관본 지정** — 특정 스탬프의 보관본만 쓴다. 아무거나 집지 않는다.
  · **기본 미리보기** — `--apply` 를 붙이기 전에는 아무것도 쓰지 않는다.

## 사용

    .venv/bin/python restore_invalidated_checkpoints.py <project_id> <episode_id> \\
        --stamp 20260807_002530 --exclude scene_director,shot_director
    # 위 출력을 확인한 뒤
    ... --apply
"""
from __future__ import annotations

import argparse
import shutil
import sys
from pathlib import Path

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

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


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("project_id")
    ap.add_argument("episode_id")
    ap.add_argument("--stamp", required=True,
                    help="보관본 스탬프 (manifest_<stamp>.json)")
    ap.add_argument("--exclude", default="",
                    help="되돌리지 않을 스텝 (쉼표 구분)")
    ap.add_argument("--apply", action="store_true", help="실제로 되돌린다")
    a = ap.parse_args()

    excluded = {s.strip() for s in a.exclude.split(",") if s.strip()}
    base = (Path(settings.projects_dir) / a.project_id
            / "checkpoints" / "episodes" / a.episode_id)
    if not base.is_dir():
        sys.exit(f"체크포인트 디렉토리 없음: {base}")

    restore, skip_exists, skip_excl, no_archive = [], [], [], []
    for d in sorted(p for p in base.iterdir() if p.is_dir()):
        sid = d.name
        live, arch = d / "manifest.json", d / f"manifest_{a.stamp}.json"
        if sid in excluded:
            skip_excl.append(sid)
        elif live.exists():
            skip_exists.append(sid)          # ★추가 전용 — 절대 덮어쓰지 않는다
        elif arch.exists():
            restore.append((sid, arch, live))
        else:
            no_archive.append(sid)

    print(f"대상 : {base}")
    print(f"보관본 스탬프 : {a.stamp}")
    print(f"되돌릴 스텝        : {len(restore)}")
    print(f"이미 manifest 있음 : {len(skip_exists)}  (건드리지 않음)")
    print(f"표적이라 제외      : {len(skip_excl)}  {sorted(skip_excl)}")
    print(f"보관본 없음        : {len(no_archive)}  {sorted(no_archive)[:8]}")
    print()
    for sid, arch, _ in restore:
        print(f"  {sid:34} ← {arch.name} ({arch.stat().st_size:,}B)")

    if not a.apply:
        print("\n미리보기다. 실제로 되돌리려면 --apply 를 붙여라.")
        return

    done = 0
    for sid, arch, live in restore:
        if live.exists():                    # 경합 대비 재확인
            print(f"  건너뜀(그새 생김): {sid}")
            continue
        shutil.copy2(arch, live)
        done += 1
    print(f"\n되돌림 {done}개. 보관본은 그대로 남겨 둔다.")


if __name__ == "__main__":
    main()
