#!/usr/bin/env python3
"""Assemble Kenerate clips into one edit: trim, normalize to one format, join with transitions, add a ducked music
bed, burn captions, export an H.264 MP4.

Examples
  python edit.py --clip https://.../a.mp4@0-4.5 --clip b.mp4 --clip c.png --aspect 9:16 \
      --music bed.mp3 --captions "Meet the bar that runs as far as you do" --out kenerate-out/ad_9x16.mp4
  python edit.py --clips-json edit.json          # {"clips":[{"src":"...","in":0,"out":4}], "music":"...", ...}

Each --clip is a URL or file, optionally with @IN-OUT seconds (a.mp4@1.2-5, a.mp4@2-, a.mp4@-3).
Stills become --still-seconds long shots.
"""
from __future__ import annotations

import argparse
import json
import shutil
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import kmedia as K  # noqa: E402


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--clip", action="append", default=[], help="URL or file, optional @IN-OUT seconds; repeat in order")
    ap.add_argument("--clips-json", help="JSON file: {clips:[{src,in,out}], music, captions, srt, aspect, ...}")
    ap.add_argument("--aspect", default="9:16", help="9:16, 16:9, 1:1, 4:5 or WxH (default 9:16)")
    ap.add_argument("--res", type=int, default=1080, help="short side in px (default 1080)")
    ap.add_argument("--fps", type=int, default=30)
    ap.add_argument("--fit", choices=["cover", "contain", "blur"], default="cover",
                    help="cover = fill and crop (default), contain = letterbox, blur = fit over a blurred fill")
    ap.add_argument("--transition", default="fade", help="cut, fade, dissolve, wipeleft, slideup, circleopen, ... (default fade)")
    ap.add_argument("--xfade", type=float, default=0.4, help="transition seconds (default 0.4)")
    ap.add_argument("--still-seconds", type=float, default=3.0)
    ap.add_argument("--mute-clips", action="store_true", help="drop the clips' own audio")
    ap.add_argument("--music", help="music bed URL or file (looped / trimmed to the edit)")
    ap.add_argument("--music-db", type=float, default=-14.0, help="music level in dB (default -14)")
    ap.add_argument("--no-duck", action="store_true", help="don't lower the music under speech")
    ap.add_argument("--srt", help="SRT / VTT captions to burn (timed to the final edit)")
    ap.add_argument("--captions", help="script text to burn as auto-timed captions over the whole edit")
    ap.add_argument("--caption-style", choices=K.CAPTION_STYLES, default="bold")
    ap.add_argument("--caption-pos", choices=["bottom", "center", "top"], default="bottom")
    ap.add_argument("--voiceover", help="voice-over URL or file (only one the user asked for), mixed over the clips")
    ap.add_argument("--vo-start", type=float, default=0.0, help="voice-over starts at N s (default 0)")
    ap.add_argument("--vo-db", type=float, default=0.0, help="voice-over level in dB (default 0)")
    ap.add_argument("--clip-db", type=float, default=-12.0, help="clips' own sound under a voice-over, dB (default -12)")
    K.add_font_args(ap, "TTF/OTF for captions")
    ap.add_argument("--preview", action="store_true", help="also write a contact sheet of the finished edit")
    ap.add_argument("--fade", type=float, default=0.3, help="fade from / to black at start and end (0 = off)")
    ap.add_argument("--out", default="kenerate-out/video-editing/edit.mp4")
    ap.add_argument("--keep-temp", action="store_true")
    a = K.parse_args(ap)

    spec = {}
    if a.clips_json:
        spec = json.loads(Path(a.clips_json).read_text(encoding="utf-8"))
    clips = [(c["src"], c.get("in"), c.get("out")) for c in spec.get("clips", [])] + [K.parse_span(c) for c in a.clip]
    if not clips:
        K.fail("No clips given.", "Pass --clip URL_OR_FILE (repeat it), or --clips-json.")
    for key in ("music", "voiceover", "srt", "captions", "aspect", "transition"):
        if spec.get(key) and getattr(a, key) in (None, ap.get_default(key)):
            setattr(a, key, spec[key])

    st = K.Status(7, "video-editing")
    K.require(status=st)
    W, H = K.size_for(a.aspect, a.res)
    tmp = K.workdir(a.keep_temp)
    out = Path(a.out)
    out.parent.mkdir(parents=True, exist_ok=True)

    st.step(f"Fetching {len(clips)} clip(s){' + music' if a.music else ''}{' + voice-over' if a.voiceover else ''}")
    srcs = K.fetch_all([c[0] for c in clips], tmp / "in", st, prefix="clip")
    music = K.fetch(a.music, tmp / "in", "music", st) if a.music else None
    vo = K.fetch(a.voiceover, tmp / "in", "voiceover", st) if a.voiceover else None

    st.step(f"Normalizing clips to {W}x{H} @ {a.fps} fps ({a.fit})")
    norm = []
    for i, (p, (_, cin, cout)) in enumerate(zip(srcs, clips), 1):
        st.info(f"clip {i}/{len(srcs)}: {p.name}" + (f" [{cin or 0}-{cout if cout is not None else 'end'}]" if cin or cout else ""))
        start = cin or 0.0
        dur = (cout - start) if cout is not None else None
        if dur is not None and dur <= 0:
            K.fail(f"Clip {i} has an empty range ({cin}-{cout}).", "Use @IN-OUT with OUT > IN.")
        dst = tmp / f"norm_{i:02d}.mp4"
        d = K.normalize_clip(p, dst, W, H, a.fps, start, dur, a.fit, mute=a.mute_clips, status=st,
                             still_seconds=a.still_seconds, move=i)
        norm.append((dst, d))

    st.step(f"Joining {len(norm)} clip(s) ({a.transition}{'' if a.transition == 'cut' else f' {a.xfade}s'})")
    joined = tmp / "joined.mp4"
    total = K.join_clips(norm, joined, a.transition, a.xfade, st)
    cur = joined

    st.step("Voice-over" + ("" if vo else " (none)"))
    if vo:
        vod = K.probe(vo)["duration"]
        if a.vo_start + vod > total + 0.3:
            st.warn(f"the voice-over ({vod:.1f} s from {a.vo_start:.1f} s) is longer than the edit ({total:.1f} s): it is cut at the end")
        ms = int(a.vo_start * 1000)
        cg = 0.0 if a.mute_clips else 10 ** (a.clip_db / 20)
        vg = 10 ** (a.vo_db / 20)
        voiced = tmp / "voiced.mp4"
        K.run_ffmpeg(["-i", str(cur), "-i", str(vo), "-filter_complex",
                      f"[0:a]volume={cg:.4f}[c];[1:a]aresample=48000,aformat=channel_layouts=stereo,adelay={ms}|{ms},volume={vg:.4f}[v];"
                      "[c][v]amix=inputs=2:duration=first:dropout_transition=0:normalize=0,alimiter=limit=0.95[a]",
                      "-map", "0:v", "-map", "[a]", "-c:v", "copy"] + K.AUDIO_ARGS + [str(voiced)], duration=total, status=st)
        cur = voiced
    st.step("Music bed" + ("" if music else " (skipped)"))
    if music:
        mixed = tmp / "mixed.mp4"
        K.mix_music(cur, music, mixed, a.music_db, duck=not a.no_duck, keep_voice=not a.mute_clips or bool(vo), status=st)
        cur = mixed

    cues = K.cues_from_args(a.srt, a.captions, total)
    st.step(f"Captions ({len(cues)} cues, {a.caption_style})" if cues else "Captions (skipped)")
    if cues:
        lst = K.caption_track(cues, W, H, total, tmp / "caps", a.caption_style, a.caption_pos, a.font, status=st)
        capped = tmp / "captioned.mp4"
        K.overlay_track(cur, lst, capped, st)
        cur = capped
        K.write_srt(cues, out.with_suffix(".srt"))

    st.step("Final export")
    if a.fade > 0:
        f = min(a.fade, total / 4)
        K.run_ffmpeg(["-i", str(cur), "-vf", f"fade=t=in:st=0:d={f:.2f},fade=t=out:st={total - f:.3f}:d={f:.2f}",
                      "-af", f"afade=t=in:st=0:d={f:.2f},afade=t=out:st={total - f:.3f}:d={f:.2f}"]
                     + K.video_codec_args(20) + K.AUDIO_ARGS + [str(out)], duration=total, status=st)
    else:
        shutil.copyfile(cur, out)
    info = K.file_info(out)
    st.info(f"wrote {out} ({info.get('size')}, {info.get('duration')} s)")
    sheet = K.video_preview(out, out.with_name(out.stem + "_preview.jpg"), tmp=tmp / "pv") if a.preview else None
    K.finish({"output": info, "srt": str(out.with_suffix('.srt').resolve()) if cues else None,
              "clips": len(norm), "frame": f"{W}x{H}", "preview": str(sheet.resolve()) if sheet else None,
              "audio": {"clips": not a.mute_clips, "voiceover": bool(vo), "music": bool(music)}, "seconds": round(total, 2), "elapsed_s": st.elapsed(),
              "temp": str(tmp) if a.keep_temp else None})


if __name__ == "__main__":
    K.main_guard(main)
