#!/usr/bin/env python3
"""motion_render.py — render a clip through a Remotion motion composition (animation + cuts).

Pre-cuts the source segment into compositions/motion/public, extracts clip-relative word timings,
writes props, and runs `npx remotion render`. First capability: KineticCaptions (word-punch karaoke
captions + animated hook + handle) over the live footage.

CLI: python3 motion_render.py --source V --start S --end E --out O [--words W.json] [--hook ".."] [--comp KineticCaptions]
"""
import argparse
import json
import os
import subprocess
from pathlib import Path

HERE = Path(__file__).resolve().parent          # tools/
STUDIO = HERE.parent
MOTION = STUDIO / "compositions" / "motion"
PUBLIC = MOTION / "public"
FFMPEG = "/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg"
FFPROBE = "/opt/homebrew/opt/ffmpeg-full/bin/ffprobe"


def probe_src(source):
    """(width, height, fps) of the source — so renders match it instead of forcing 1080x1920@30."""
    try:
        o = subprocess.run([FFPROBE, "-v", "error", "-select_streams", "v:0",
                            "-show_entries", "stream=width,height,r_frame_rate", "-of", "csv=p=0",
                            str(source)], capture_output=True, text=True, timeout=30).stdout.strip()
        w, h, fr = o.split(",")[:3]
        num, den = (fr.split("/") + ["1"])[:2]
        fps = float(num) / float(den or 1)
        return int(w), int(h), round(fps, 3)
    except Exception:
        return 1920, 1080, 30.0


def clip_words(words_path, start, end):
    try:
        words = json.loads(Path(words_path).read_text()).get("words", [])
    except Exception:
        return []
    out = []
    for w in words:
        if w.get("type", "word") != "word":
            continue
        s = w.get("start", 0)
        if start - 0.05 <= s <= end + 0.05:
            out.append({"start": round(s - start, 3), "end": round(w.get("end", s) - start, 3),
                        "text": w.get("text", "")})
    return out


def render(source, start, end, out, words_path=None, hook="", handle="@19keys",
           accent="#E4C56B", fps=None, comp="KineticCaptions", extra=None):
    start, end = float(start), float(end)
    dur = end - start
    # HOUSE RULE: never downscale, match source fps. Comps are authored 1080x1920;
    # Remotion --scale multiplies the canvas, so a 4K source renders at 2160-tall, 60fps stays 60.
    src_w, src_h, src_fps = probe_src(source)
    if not fps:
        fps = src_fps if src_fps and 10 <= src_fps <= 120 else 30.0
    crop_h = min(src_h, int(src_w * 16 / 9))            # true vertical pixels available from a 9:16 crop
    scale = max(1.0, min(2.0, round(crop_h / 1920, 3)))  # 1080p src → 1.0; 4K src → 1.125..2.0
    W, H = int(1080 * scale) // 2 * 2, int(1920 * scale) // 2 * 2
    cid = f"{int(start * 1000)}_{int(end * 1000)}"
    clip_name = f"clip_{cid}.mp4"
    PUBLIC.mkdir(parents=True, exist_ok=True)
    clip_path = PUBLIC / clip_name
    subprocess.run([FFMPEG, "-y", "-ss", str(start), "-to", str(end), "-i", str(source),
                    "-vf", f"scale={W}:{H}:force_original_aspect_ratio=increase,crop={W}:{H},setsar=1",
                    "-r", str(fps), "-c:v", "libx264", "-pix_fmt", "yuv420p",
                    "-c:a", "aac", "-b:a", "160k", str(clip_path)], capture_output=True)
    if not clip_path.exists():
        raise RuntimeError("pre-cut failed")
    # "clip" (KineticCaptions) and "src" (VoxEditorial) both point at the cut footage
    props = {"clip": clip_name, "src": clip_name, "isImage": False,
             "fps": fps, "durationInFrames": max(1, round(dur * fps)),
             "words": clip_words(words_path, start, end) if words_path else [],
             "hook": hook, "handle": handle, "accent": accent}
    if comp == "AudioWaveform":
        audio_name = f"audio_{cid}.mp3"
        subprocess.run([FFMPEG, "-y", "-i", str(clip_path), "-vn",
                        "-c:a", "libmp3lame", "-q:a", "4", str(PUBLIC / audio_name)], capture_output=True)
        props["audio"] = audio_name
    if extra:
        props.update(extra)
    propf = MOTION / f"props_{cid}.json"
    propf.write_text(json.dumps(props))
    out = Path(out); out.parent.mkdir(parents=True, exist_ok=True)
    env = dict(os.environ); env["PATH"] = "/opt/homebrew/bin:" + env.get("PATH", "")
    cmd = ["npx", "remotion", "render", "src/index.ts", comp, str(out),
           f"--props={propf.name}", "--concurrency=2"]
    if scale > 1.0:
        cmd.append(f"--scale={scale}")                   # render the canvas at source-true resolution
    r = subprocess.run(cmd, cwd=str(MOTION), env=env, capture_output=True, text=True, timeout=1800)
    try:
        propf.unlink()
    except OSError:
        pass
    if not out.exists():
        raise RuntimeError("remotion failed: " + (r.stderr or r.stdout or "")[-700:])
    return str(out)


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--source", required=True)
    ap.add_argument("--start", type=float, required=True)
    ap.add_argument("--end", type=float, required=True)
    ap.add_argument("--out", required=True)
    ap.add_argument("--words", default=None)
    ap.add_argument("--hook", default="")
    ap.add_argument("--handle", default="@19keys")
    ap.add_argument("--accent", default="#E4C56B")
    ap.add_argument("--comp", default="KineticCaptions")
    a = ap.parse_args()
    print(render(a.source, a.start, a.end, a.out, words_path=a.words, hook=a.hook,
                 handle=a.handle, accent=a.accent, comp=a.comp))
