"""뷰어의 턴 목록(outputs/turns/index.json)을 실제 디렉터리에서 다시 만든다.

왜 필요한가 (2026-08-06): viewer.html:489가 이 파일을 읽어 좌측 버튼을 만드는데,
발행 스크립트(render_solo_*_turn.py)들은 턴 디렉터리와 meta.json만 쓰고 이 목록은
갱신하지 않는다. 그래서 새로 발행한 턴이 뷰어에 **아예 나타나지 않는다** — 서버도
파일도 멀쩡한데 볼 방법이 없다. 실제로 index.json이 8/5 14:35에 멈춰 GEN_r04b·
GEN_r04c가 안 보였다.

정렬: 진단 사다리 순서(GT → 코덱 왕복 → 생성)로 앞에 모은다. 같은 클립을 위아래로
눌러 비교하는 게 이 뷰어의 용법이라 파일시스템 순서로 흩어놓으면 쓸모가 없다.

    python rebuild_turn_index.py            # 다시 만들기
    python rebuild_turn_index.py --dry-run  # 뭐가 들어갈지만 보기
"""
import argparse
import json
import os

HERE = os.path.dirname(os.path.abspath(__file__))
TURNS = os.path.join(HERE, "outputs", "turns")
REQUIRED = ("meta.json", "face.json", "body.vrma", "audio.wav")

# 앞에 오는 순서. 진단 사다리: 원본 → 코덱만 → 생성기.
# 접두사로 맞추고, 목록에 없는 건 뒤에 이름순으로 붙인다.
PRIORITY = ("GT_", "CODEC_", "MIRROR_", "GEN_")


def rank(turn_id: str):
    for i, p in enumerate(PRIORITY):
        if turn_id.startswith(p):
            return (i, turn_id)
    return (len(PRIORITY), turn_id)


def sort_key(m):
    # 디렉터리 이름으로 정렬한다 — m["id"]에는 "[ep699]"가 붙어 있어
    # 문자열 정렬이 에폭 숫자에 흔들린다
    return rank(os.path.basename(m["dir"]))


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--dry-run", action="store_true")
    args = ap.parse_args()

    entries, skipped = [], []
    for name in sorted(os.listdir(TURNS)):
        d = os.path.join(TURNS, name)
        if not os.path.isdir(d):
            continue
        missing = [f for f in REQUIRED if not os.path.isfile(os.path.join(d, f))]
        if missing:
            skipped.append((name, ",".join(missing)))
            continue
        m = json.load(open(os.path.join(d, "meta.json")))
        m["dir"] = f"outputs/turns/{name}"
        # 버튼에 **에폭을 항상 붙인다.** best.pth는 런마다 다른 에폭이라(실측
        # r03 2899 / r04b 699 / r04c 299) 이름만 보고 나란히 누르면 학습량이
        # 10배 차이나는 것을 "구조 차이"로 오독한다. 실제로 그랬다(2026-08-06).
        # viewer.html은 m.id를 표시에만 쓰고 파일은 m.dir로 찾으므로 안전하다.
        ep = m.get("generator_epoch")
        m["id"] = f"{name}  [ep{ep}]" if ep is not None else name
        m.setdefault("emotion", "neutral")
        m.setdefault("fps", 30)
        # renderMeta가 읽는 값들 — 없으면 화면에 undefined가 뜬다
        m.setdefault("face_frames", 0)
        m.setdefault("timing", {})
        entries.append(m)

    entries.sort(key=sort_key)

    print(f"{len(entries)}개 턴:")
    for m in entries:
        run = m.get("run_id", "")
        print(f"  {m['id']:<34} {run}")
    if skipped:
        print(f"\n제외 {len(skipped)}개 (필수 파일 없음):")
        for n, why in skipped:
            print(f"  {n:<32} 없음: {why}")

    if args.dry_run:
        return
    out = os.path.join(TURNS, "index.json")
    with open(out, "w") as f:
        json.dump(entries, f, ensure_ascii=False, indent=1)
    print(f"\n{out}")


if __name__ == "__main__":
    main()
