"""Batch runner: streaming multi-view weather transfer over EVERY sequence in a validation split.

Loads the keyframe + interpolation pipes ONCE (via build_pipes) and loops run_sequence over all
sequences, so the ~19 GB of weights + UMT5 text encoding are paid a single time instead of per
sequence. Resumable: a sequence whose grid.mp4 already exists is skipped. Per-sequence failures are
logged and skipped so one bad clip doesn't kill the whole run.

Videos are expected pre-downloaded on disk as:  <data-root>/<seq>/<camera>.mp4
(the seq id may contain '/', e.g. 'chunk_00.parquet/ip602_...'). Outputs go to <out>/<seq>/.

Run inside the cosmos-predict2-local docker with A4D_WAN_FORCE_SDPA=1 and PYTHONPATH=/workspace
(see custom/eval/vid2vid_eval/README.md). Example:

  A4D_WAN_FORCE_SDPA=1 PYTHONPATH=/workspace python custom/eval/vid2vid_eval/batch_val_transfer.py \
    --kf-ckpt /assets/ckpts/kf.pt --inp-ckpt /assets/ckpts/inp.pt \
    --wan-dir /assets/wan --vae-path /assets/wan/Wan2.1_VAE.pth \
    --split-json /assets/split_val_transfer.json --data-root /assets/val_videos \
    --cameras front_wide front_tele --num-frames 100 --out /assets/output/val_transfer
"""

import argparse
import json
import os
import sys
import time
import traceback

# repo root = .../Any4D ; this file lives at custom/eval/vid2vid_eval/ (4 levels down)
ANY4D_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
if ANY4D_ROOT not in sys.path:
    sys.path.insert(0, ANY4D_ROOT)

from custom.eval.vid2vid_eval.infer_multiview_video_transfer import (
    build_pipes, run_sequence, SNOW_PROMPT, NEG_PROMPT)


def _load_seqs(split_json, seqs_file):
    if seqs_file:
        return [l.strip() for l in open(seqs_file) if l.strip()]
    return list(json.load(open(split_json))['sequences'].keys())


def main():
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument('--kf-ckpt', required=True)
    p.add_argument('--inp-ckpt', required=True)
    p.add_argument('--wan-dir', required=True)
    p.add_argument('--vae-path', default=None)
    p.add_argument('--kf-model', choices=['anchor', 'syncfz'], default='anchor',
                   help='keyframe backend (anchor=cross-view Fun-Control, syncfz=pure Wan-T2V).')
    p.add_argument('--split-json', default=None, help='validation split json with a "sequences" dict.')
    p.add_argument('--seqs-file', default=None, help='newline-separated seq ids (overrides --split-json).')
    p.add_argument('--data-root', required=True, help='<data-root>/<seq>/<camera>.mp4')
    p.add_argument('--cameras', nargs='+', default=['front_wide', 'front_tele'],
                   help='cameras per sequence; the FIRST is front_wide.')
    p.add_argument('--num-frames', type=int, default=100, help='10 s @ 10 fps = 100.')
    p.add_argument('--out', required=True, help='root output dir; each sequence -> <out>/<seq>/.')
    p.add_argument('--limit', type=int, default=0, help='process at most N sequences (0 = all).')
    p.add_argument('--overwrite', action='store_true', help='re-run even if <seq>/grid.mp4 exists.')
    # tuning passthroughs (defaults match the single-sequence script)
    p.add_argument('--prompt', default=SNOW_PROMPT)
    p.add_argument('--neg-prompt', default=NEG_PROMPT)
    p.add_argument('--sn-cutoff', type=float, default=20.0)
    p.add_argument('--sn-channels', type=int, default=4)
    p.add_argument('--kf-steps', type=int, default=35)
    p.add_argument('--kf-guidance', type=float, default=5.0)
    p.add_argument('--inp-steps', type=int, default=35)
    p.add_argument('--inp-guidance', type=float, default=5.0)
    p.add_argument('--seed', type=int, default=0)
    p.add_argument('--sdedit-strength', type=float, default=1.0)
    p.add_argument('--no-tone-match', action='store_true', help='disable per-keyframe tone-match (on by default).')
    p.add_argument('--front-w', type=int, default=1280)
    p.add_argument('--fps', type=int, default=10)
    args = p.parse_args()

    if not args.split_json and not args.seqs_file:
        raise SystemExit('provide --split-json or --seqs-file')
    seqs = _load_seqs(args.split_json, args.seqs_file)
    if args.limit:
        seqs = seqs[:args.limit]
    out_root = args.out if os.path.isabs(args.out) else os.path.join(ANY4D_ROOT, args.out)
    print(f'[batch] {len(seqs)} sequences, cameras={args.cameras}, num_frames={args.num_frames}')
    print(f'[batch] data-root={args.data_root}  out={out_root}')

    pipes = build_pipes(args.kf_ckpt, args.inp_ckpt, args.wan_dir, args.vae_path,
                        kf_model=args.kf_model, sn_cutoff=args.sn_cutoff, sn_channels=args.sn_channels,
                        prompt=args.prompt, neg_prompt=args.neg_prompt)

    done, skipped, failed = 0, 0, 0
    t0 = time.time()
    for si, seq in enumerate(seqs):
        out_dir = os.path.join(out_root, seq)
        grid = os.path.join(out_dir, 'grid.mp4')
        if not args.overwrite and os.path.exists(grid) and os.path.getsize(grid) > 0:
            print(f'[batch {si+1}/{len(seqs)}] SKIP (exists) {seq}')
            skipped += 1
            continue
        videos = [os.path.join(args.data_root, seq, f'{c}.mp4') for c in args.cameras]
        missing = [v for v in videos if not os.path.isfile(v)]
        if missing:
            print(f'[batch {si+1}/{len(seqs)}] FAIL (missing videos) {seq}: {missing}')
            failed += 1
            continue
        print(f'\n[batch {si+1}/{len(seqs)}] {seq}  (done={done} skip={skipped} fail={failed}, '
              f'elapsed={ (time.time()-t0)/60:.1f} min)')
        try:
            run_sequence(pipes, videos, args.num_frames, out_dir,
                         kf_steps=args.kf_steps, kf_guidance=args.kf_guidance,
                         inp_steps=args.inp_steps, inp_guidance=args.inp_guidance, seed=args.seed,
                         sdedit_strength=args.sdedit_strength, tone_match=not args.no_tone_match,
                         front_w=args.front_w, fps=args.fps)
            done += 1
        except Exception:
            failed += 1
            print(f'[batch {si+1}/{len(seqs)}] ERROR on {seq}:\n{traceback.format_exc()}')

    print(f'\n[batch DONE] done={done} skipped={skipped} failed={failed} '
          f'total_time={(time.time()-t0)/60:.1f} min -> {out_root}')


if __name__ == '__main__':
    main()
