#!/usr/bin/env python3
"""luxury_render.py — cinematic "luxury" clip treatment in 19Keys BLACK + GOLD.
Filmic grade (lifted rich blacks, gold-warm highlights, muted sat) + soft bloom +
vignette + fine grain + a thin gold frame + an elegant Didot caption with a gold
hairline + a small 19 KEYS mark. 9:16 vertical, speaker-tracked crop.

    python3 luxury_render.py --video SRC --start 2947 --end 2956 \
        --title "I CHOOSE NOT TO ENGAGE IN POLITICS" --out out.mp4
"""
import argparse
import hashlib
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

FF = "/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg"
FFPROBE = "/opt/homebrew/opt/ffmpeg-full/bin/ffprobe"   # NOT FF.replace("ffmpeg","ffprobe"):
                                                        # that also mangles "ffmpeg-full" in the path
DIDOT = "Didot"                      # libass matches by family name
GOLD = "0xD4AF37"                    # brand gold (ffmpeg 0xRRGGBB)
CREAM = "&H00E8ECF2"                 # ASS BGR cream-white
GOLD_ASS = "&H0037AFD4"              # ASS BGR of #D4AF37

# Two registers of luxury:
#  cinematic — rich, moody, framed (A24-ish); gold in the highlights, gold frame.
#  quiet     — "The Blue" quiet luxury: muted cohesive grade, near-monochrome, extreme
#              restraint — no frame, one hairline, a whisper of gold. Power via understatement.
MODES = {
    "cinematic": {
        "grade": ("curves=r='0/0.0 0.5/0.5 0.85/0.9 1/1':g='0/0.0 0.5/0.49 1/0.98':b='0/0.012 0.5/0.475 1/0.95',"
                  "eq=contrast=1.12:saturation=0.93:gamma=0.97:brightness=-0.02,"
                  "colorbalance=rh=0.05:gh=0.035:bh=-0.06"),
        "bloom": "split[b0][b1];[b1]gblur=sigma=12[bl];[b0][bl]blend=all_mode=screen:all_opacity=0.13",
        "film": "vignette=PI/4.2,noise=c0s=5:c0f=t+u",
        "frame": True, "cap_size": 58, "cap_spacing": 3.5, "cap_marginv": 300,
    },
    "quiet": {
        "grade": ("curves=r='0/0.015 0.5/0.5 1/0.985':g='0/0.015 0.5/0.5 1/0.985':b='0/0.02 0.5/0.5 1/0.985',"
                  "eq=contrast=1.05:saturation=0.78:gamma=0.99:brightness=-0.015,"
                  "colorbalance=rh=0.03:gh=0.02:bh=-0.03:rm=0.01:bm=-0.01"),
        "bloom": "split[b0][b1];[b1]gblur=sigma=9[bl];[b0][bl]blend=all_mode=screen:all_opacity=0.08",
        "film": "vignette=PI/5.5,noise=c0s=3:c0f=t+u",
        "frame": False, "cap_size": 50, "cap_spacing": 5.5, "cap_marginv": 250,
    },
}


def _ass(title, dur, w, h, size, spacing, marginv, font=DIDOT):
    """Letter-spaced caption (Didot by default; any installed family via --font), centered
    lower-third, slow fade + a whisper 19 KEYS. libass resolves `font` by family name."""
    end = f"{int(dur//3600)}:{int(dur//60)%60:02d}:{dur%60:05.2f}"
    q = title.replace("{", "(").replace("}", ")")
    fn = (font or DIDOT).replace(",", " ")            # ASS Fontname is a bare family name
    return (
        f"[Script Info]\nScriptType: v4.00+\nPlayResX: {w}\nPlayResY: {h}\nWrapStyle: 0\n"
        "ScaledBorderAndShadow: yes\n\n[V4+ Styles]\n"
        "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, "
        "Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, "
        "Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
        f"Style: LUX,{fn},{size},{CREAM},{CREAM},&H00000000,&H64000000,0,0,0,0,100,100,{spacing},0,1,0,4,2,120,120,{marginv},1\n"
        f"Style: MARK,{DIDOT},27,{GOLD_ASS},{GOLD_ASS},&H00000000,&H00000000,0,1,0,0,100,100,6,0,1,0,0,8,0,60,70,1\n\n"
        "[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
        f"Dialogue: 0,0:00:00.00,{end},LUX,,0,0,0,,{{\\fad(700,300)}}{q}\n"
        f"Dialogue: 0,0:00:00.00,{end},MARK,,0,0,0,,{{\\fad(900,300)}}19 KEYS\n"
    )


_HERE = Path(__file__).resolve().parent
_LUTS = _HERE / "luts"
# three grades from 19Keys' own LUTs (copied into the studio → portable)
GRADES = {"cool": _LUTS / "cool.cube",       # teal/muted — The Blue register (default)
          "warm": _LUTS / "warm.cube",        # rust/gold-rich
          "natural": _LUTS / "natural.cube"}  # clean, refined, understated
DEFAULT_GRADE = "cool"
REFRAME_PY = str(_HERE / "reframe-venv" / "bin" / "python")
REFRAME = str(_HERE / "reframe.py")

# ---- masked luxury grade v2 (lux2 / luxury-masked) ---------------------------------
_MASKS = _HERE / "masks"                       # cached power-window ellipse PNGs
_MATTE_CACHE = _HERE / ".matte-cache"          # cached geo.mp4 + matte.mp4 per clip window
_PM_SRC = _HERE / "personmatte.swift"          # Apple Vision person-matte CLI (source)
_PM_BIN = _HERE / "personmatte"                # compiled on first use via swiftc
_ZP_VERSION = "zp-v1"                          # bump when the zoompan expression changes


def _face_crop(video, start, end, W, H):
    """Speaker-tracked crop geometry (falls back to centre)."""
    try:
        import json as _j
        r = subprocess.run([REFRAME_PY, REFRAME, "--video", str(video), "--start", str(start),
                            "--end", str(end), "--aspect", f"{W}:{H}"], capture_output=True, text=True, timeout=120)
        g = _j.loads(r.stdout or "{}").get("crop")
        if g:
            return f"crop={g}"
    except Exception:
        pass
    return f"crop='min(iw,ih*{W}/{H})':'min(ih,iw*{H}/{W})':(iw-ow)/2:(ih-oh)/2"


def render_pure(video, start, end, out, aspect="9:16", grade="cool", lut=None):
    """The purest cut — your real LUT, composed crop, a slow push-in, film halation +
    grain + a whisper of vignette. NO caption, NO logo, NO frame. Pure image, pure restraint."""
    W, H = (1080, 1920) if aspect == "9:16" else (1080, 1080) if aspect == "1:1" else (1920, 1080)
    dur = round(float(end) - float(start), 2)
    # resolve the LUT: explicit path > named grade > default; fall back to a light built-in grade
    lut_path = Path(lut) if lut else GRADES.get(grade, GRADES[DEFAULT_GRADE])
    if lut_path and lut_path.exists():
        lut_esc = str(lut_path).replace(":", r"\:").replace("'", r"\'")
        color = f"lut3d='{lut_esc}'"
    else:
        color = "eq=contrast=1.08:saturation=0.85:gamma=0.98,colorbalance=rh=0.03:bh=-0.04"  # graceful fallback
    fc = _face_crop(video, start, end, W, H)
    # source fps (keep it — 24fps reads cinematic; avoids judder from a resample)
    fps = _probe_fps(video)
    chain = [
        fc, f"scale={W}:{H}",
        color,
        # slow ~5% push-in (centered) — the "expensive" motion, via zoompan
        f"zoompan=z='min(zoom+0.0004,1.05)':d=1:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s={W}x{H}:fps={fps}",
        "split[p0][p1];[p1]gblur=sigma=14[pb];[p0][pb]blend=all_mode=screen:all_opacity=0.10",  # halation
        "vignette=PI/5.5,noise=c0s=4:c0f=t+u",
    ]
    vf = ",".join(chain)
    r = subprocess.run(
        [FF, "-y", "-ss", str(start), "-i", str(video), "-t", str(dur),
         "-vf", vf, "-c:v", "libx264", "-crf", "17", "-preset", "slow",
         "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "192k", str(out)],
        capture_output=True, text=True, timeout=1800)
    if not Path(out).exists():
        raise RuntimeError("pure render failed: " + (r.stderr or "")[-500:])
    return str(out)


# ======================================================================================
# masked luxury grade v2
#   TIER 1  render_lux2    — pure ffmpeg: luminance split-tone (gold highlights / cool
#           shadows through soft geq luma masks), skin-protected saturation (reds+yellows
#           excluded by hue qualifier), soft elliptical power-window lift post-zoompan,
#           then the existing halation + vignette + grain film layer.
#   TIER 2  render_masked  — Apple Vision person matte drives a dual grade: subject
#           gold-warm +0.10 stop, world cool -0.20 stop / -40% sat, merged with a
#           feathered matte (maskedmerge). Falls back to Tier 1 if the matte fails.
# ======================================================================================

def _dims(aspect):
    return (1080, 1920) if aspect == "9:16" else (1080, 1080) if aspect == "1:1" else (1920, 1080)


def _probe_fps(video):
    try:
        fr = subprocess.run([FFPROBE, "-v", "error", "-select_streams", "v:0",
                             "-show_entries", "stream=r_frame_rate", "-of", "csv=p=0", str(video)],
                            capture_output=True, text=True, timeout=30).stdout.strip().split("/")
        return round(float(fr[0]) / float(fr[1] if len(fr) > 1 else 1)) or 24
    except Exception:
        return 24


def _nb_frames(video):
    """Frame count of a stream; nb_frames metadata first, packet-count fallback."""
    fp = FFPROBE
    r = subprocess.run([fp, "-v", "error", "-select_streams", "v:0", "-show_entries",
                        "stream=nb_frames", "-of", "csv=p=0", str(video)],
                       capture_output=True, text=True, timeout=30).stdout.strip()
    if r.isdigit():
        return int(r)
    r = subprocess.run([fp, "-v", "error", "-select_streams", "v:0", "-count_packets",
                        "-show_entries", "stream=nb_read_packets", "-of", "csv=p=0", str(video)],
                       capture_output=True, text=True, timeout=120).stdout.strip()
    return int(r) if r.isdigit() else -1


def _zoompan(W, H, fps):
    """The slow 5% push-in. BYTE-IDENTICAL everywhere (v1, lux2, tier-2 geometry bake) —
    the matte is only valid against the exact same motion."""
    return (f"zoompan=z='min(zoom+0.0004,1.05)':d=1:x='iw/2-(iw/zoom/2)'"
            f":y='ih/2-(ih/zoom/2)':s={W}x{H}:fps={fps}")


def _lut_filter(grade, lut=None):
    """Resolve grade/--lut to a lut3d filter string (escaped), eq fallback if missing."""
    lut_path = Path(lut) if lut else GRADES.get(grade, GRADES[DEFAULT_GRADE])
    if lut_path and lut_path.exists():
        esc = str(lut_path).replace(":", r"\:").replace("'", r"\'")
        return f"lut3d='{esc}'"
    return "eq=contrast=1.08:saturation=0.85:gamma=0.98,colorbalance=rh=0.03:bh=-0.04"


def _ellipse_mask(W, H, cy=0.40):
    """Soft elliptical power window, rendered ONCE per resolution and cached as PNG.
    Center 0.5W x cy*H, radii 0.42W/0.30H, wide 0.60-span feather — 'start wide and
    very soft'. cy=0.40 for 9:16 talking head, 0.50 for 1:1/16:9."""
    _MASKS.mkdir(parents=True, exist_ok=True)                     # verify correction
    p = _MASKS / f"ellipse_{W}x{H}_cy{int(cy * 100)}.png"
    if p.exists():
        return p
    src = (f"color=black:s={W}x{H},format=gray,"
           f"geq=lum='255*clip((1.30-sqrt(pow((X-W/2)/(0.42*W),2)"
           f"+pow((Y-{cy}*H)/(0.30*H),2)))/0.60,0,1)'")
    r = subprocess.run([FF, "-y", "-f", "lavfi", "-i", src, "-frames:v", "1",
                        "-update", "1", str(p)],                  # verify correction: -update 1
                       capture_output=True, text=True, timeout=120)
    if not p.exists():
        raise RuntimeError("ellipse mask gen failed: " + (r.stderr or "")[-300:])
    return p


def render_lux2(video, start, end, out, aspect="9:16", grade="cool", lut=None):
    """TIER 1 — masked luxury v2, pure ffmpeg. Node order per the colorist canon:
    secondaries (skin-protected sat, luma split-tone) BEFORE the LUT look, power-window
    lift after zoompan (screen-space), film texture strictly last (hides mask seams).
    Midtones — where skin lives — are excluded from the split-tone by construction."""
    W, H = _dims(aspect)
    dur = round(float(end) - float(start), 2)
    fps = _probe_fps(video)
    color = _lut_filter(grade, lut)
    wm = _ellipse_mask(W, H, 0.40 if aspect == "9:16" else 0.50)
    fc = _face_crop(video, start, end, W, H)
    fg = (
        # out_range=full: geq split points 150/90 are range-dependent — normalize so
        # limited-range sources (tv) hit the same Log-wheel splits (verify correction)
        f"[0:v]{fc},scale={W}:{H}:out_range=full,format=gbrp,"
        "huesaturation=saturation=-0.25:colors=g+c+b+m:strength=8:lightness=1,"
        "huesaturation=hue=-6:saturation=-0.10:colors=b+c:strength=6:lightness=1,"
        "split=4[base][gold_in][cool_in][m_in];"
        "[gold_in]colorbalance=rh=0.15:gh=0.06:bh=-0.12:pl=1[gold];"
        "[cool_in]colorbalance=rs=-0.06:bs=0.10:pl=1,eq=saturation=0.90[cool];"
        "[m_in]format=gray,split=2[hm_in][sm_in];"
        "[hm_in]geq=lum='clip((lum(X,Y)-150)*2.5,0,255)',gblur=sigma=4,format=gbrp[hm];"
        "[sm_in]geq=lum='clip((90-lum(X,Y))*2.5,0,255)',gblur=sigma=4,format=gbrp[sm];"
        "[base][gold][hm]maskedmerge[t1];"
        "[t1][cool][sm]maskedmerge[st];"
        f"[st]{color}[look];"
        f"[look]{_zoompan(W, H, fps)},format=gbrp[zp];"
        "[1:v]format=gbrp[wm];"
        "[zp]split=2[w0][w1];[w1]exposure=exposure=0.18[lift];"
        "[w0][lift][wm]maskedmerge[win];"
        "[win]split=2[p0][p1];[p1]gblur=sigma=14[pb];"
        # pin yuv420p BEFORE vignette,noise so grain semantics are deterministic
        # (verify correction — vignette forced it implicitly before)
        "[p0][pb]blend=all_mode=screen:all_opacity=0.10,format=yuv420p,"
        "vignette=PI/5.5,noise=c0s=4:c0f=t+u[v]"
    )
    r = subprocess.run(
        [FF, "-y", "-ss", str(start), "-i", str(video), "-i", str(wm), "-t", str(dur),
         "-filter_complex", fg, "-map", "[v]", "-map", "0:a?",
         "-c:v", "libx264", "-crf", "17", "-preset", "slow",
         "-c:a", "aac", "-b:a", "192k", str(out)],
        capture_output=True, text=True, timeout=1800)
    if not Path(out).exists():
        raise RuntimeError("lux2 render failed: " + (r.stderr or "")[-500:])
    return str(out)


def _ensure_personmatte():
    """Compile tools/personmatte.swift on first use. None -> caller falls back to Tier 1."""
    if _PM_BIN.exists():
        return _PM_BIN
    swiftc = shutil.which("swiftc")
    if not swiftc or not _PM_SRC.exists():
        return None
    r = subprocess.run([swiftc, "-O", "-o", str(_PM_BIN), str(_PM_SRC)],
                       capture_output=True, text=True, timeout=600)
    if not _PM_BIN.exists():
        print("[luxury_render] personmatte build failed: " + (r.stderr or "")[-300:], file=sys.stderr)
        return None
    return _PM_BIN


def _geometry_pass(video, start, end, W, H, fps, dst):
    """PASS 1a — bake face-crop + scale + push-in into geo.mp4 (crf 10 intermediate).
    Every downstream frame index becomes canonical: matte sync with zoompan is free."""
    dur = round(float(end) - float(start), 2)
    fc = _face_crop(video, start, end, W, H)
    vf = f"{fc},scale={W}:{H}:out_range=full,{_zoompan(W, H, fps)}"
    r = subprocess.run(
        [FF, "-y", "-ss", str(start), "-i", str(video), "-t", str(dur),
         "-vf", vf, "-c:v", "libx264", "-crf", "10", "-preset", "veryfast",
         "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "192k", str(dst)],
        capture_output=True, text=True, timeout=900)
    if not Path(dst).exists():
        raise RuntimeError("geometry pass failed: " + (r.stderr or "")[-400:])
    return dst


def _matte_pass(geo, W, H, fps, dst):
    """PASS 1b — Vision person matte over geo.mp4, streamed raw-gray into a crf-6 encode.
    Encode pinned -pix_fmt gray -color_range pc: the matte is full-range BY CONSTRUCTION
    and tagged so (verify correction — never blind-expand limited->full). Matte stored
    RAW (no feather) so tmix/gblur stay tunable per grade."""
    binp = _ensure_personmatte()
    if not binp:
        raise RuntimeError("personmatte unavailable (swiftc or source missing)")
    p1 = subprocess.Popen([str(binp), str(geo), str(W), str(H), "accurate"],
                          stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    p2 = subprocess.Popen(
        [FF, "-y", "-f", "rawvideo", "-pix_fmt", "gray", "-s", f"{W}x{H}", "-r", str(fps),
         "-i", "-", "-c:v", "libx264", "-crf", "6", "-pix_fmt", "gray",
         "-color_range", "pc", str(dst)],
        stdin=p1.stdout, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
    p1.stdout.close()
    try:
        err2 = p2.communicate(timeout=1800)[1]      # segmentation budget: minutes, not 120s
        err1 = (p1.stderr.read() or b"").decode(errors="replace")
        p1.wait(timeout=60)
    except subprocess.TimeoutExpired:
        p1.kill(); p2.kill()
        raise RuntimeError("matte pass timed out")
    if p1.returncode != 0:
        raise RuntimeError("personmatte failed: " + err1[-400:])
    if p2.returncode != 0 or not Path(dst).exists():
        raise RuntimeError("matte encode failed: " + (err2 or b"").decode(errors="replace")[-400:])
    ng, nm = _nb_frames(geo), _nb_frames(dst)
    if ng > 0 and nm != ng:
        raise RuntimeError(f"matte frame parity broken: matte {nm} != geo {ng}")
    cov = re.search(r"coverage=([0-9.]+)", err1)
    if cov and float(cov.group(1)) < 0.02:
        raise RuntimeError(f"no person found (mean coverage {cov.group(1)})")
    return dst


def _masked_assets(video, start, end, W, H, fps):
    """Cache layer: geo.mp4 + matte.mp4 keyed on source|mtime|window|dims|fps|zoompan-ver.
    Segmentation runs once per clip window ever; re-grades are a single cheap pass 2."""
    _MATTE_CACHE.mkdir(parents=True, exist_ok=True)
    src = Path(video).resolve()
    key = hashlib.sha1(
        f"{src}|{src.stat().st_mtime}|{start}|{end}|{W}x{H}|{fps}|{_ZP_VERSION}".encode()
    ).hexdigest()[:16]
    geo = _MATTE_CACHE / f"{key}-geo.mp4"
    matte = _MATTE_CACHE / f"{key}-matte.mp4"
    if not geo.exists():
        _geometry_pass(video, start, end, W, H, fps, geo)
    if not matte.exists():
        try:
            _matte_pass(geo, W, H, fps, matte)
        except Exception:
            matte.unlink(missing_ok=True)
            raise
    return geo, matte


def render_masked(video, start, end, out, aspect="9:16", grade="cool", lut=None):
    """TIER 2 — luxury-masked-{cool,warm,natural}: person-matte dual grade.
    One LUT before the split = one cohesive base look; then the relight recipe:
    world down-and-cool (-0.20 stop, sat 0.60, blue push), subject up-and-warm
    (+0.10 stop, temp 5800 — warmth slides skin ALONG the skin-tone line, never a hue
    rotation), merged through a feathered temporally-smoothed matte. White matte selects
    the SUBJECT branch ([bg][fg][m]). Silently falls back to Tier 1 if the matte fails."""
    W, H = _dims(aspect)
    fps = _probe_fps(video)
    color = _lut_filter(grade, lut)
    try:
        geo, matte = _masked_assets(video, start, end, W, H, fps)
    except Exception as e:
        print(f"[luxury_render] matte unavailable ({e}); falling back to lux2-{grade}",
              file=sys.stderr)
        return render_lux2(video, start, end, out, aspect, grade, lut)
    fg = (
        f"[0:v]format=gbrp16le,{color},split=2[bg_in][fg_in];"
        "[bg_in]exposure=exposure=-0.20,eq=saturation=0.60,"
        "colorbalance=rs=-0.03:bs=0.06:bm=0.03:pl=1[bg];"
        "[fg_in]exposure=exposure=0.10,colortemperature=temperature=5800,eq=saturation=1.05,"
        "huesaturation=saturation=-0.06:colors=c+b+m:strength=6:lightness=1[fg];"
        # matte branch: tag full range (data IS full by construction — see _matte_pass),
        # normalize pts/fps, temporal smooth (kills matte buzz), wide spatial feather
        f"[1:v]extractplanes=y,setrange=full,setpts=PTS-STARTPTS,fps={fps},"
        "tmix=frames=3:weights='1 2 1',gblur=sigma=7,format=gbrp16le[m];"
        "[bg][fg][m]maskedmerge[mrg];"
        "[mrg]split=2[p0][p1];[p1]gblur=sigma=14[pb];"
        "[p0][pb]blend=all_mode=screen:all_opacity=0.10,format=yuv420p,"
        "vignette=PI/5.5,noise=alls=3:allf=t+u[v]"
    )
    r = subprocess.run(
        [FF, "-y", "-i", str(geo), "-i", str(matte),
         "-filter_complex", fg, "-map", "[v]", "-map", "0:a?",
         "-c:v", "libx264", "-crf", "17", "-preset", "slow", "-c:a", "copy", str(out)],
        capture_output=True, text=True, timeout=1800)
    if not Path(out).exists():
        print("[luxury_render] masked merge failed (" + (r.stderr or "")[-200:]
              + "); falling back to lux2", file=sys.stderr)
        return render_lux2(video, start, end, out, aspect, grade, lut)
    return str(out)


def render_matte_preview(video, start, end, out, aspect="9:16"):
    """QC mode — the person matte composited as a 50% red overlay on geo.mp4 so edge
    quality and flicker can be eyeballed before committing to a grade. No fallback:
    a preview that can't build its matte should fail loudly."""
    W, H = _dims(aspect)
    fps = _probe_fps(video)
    geo, matte = _masked_assets(video, start, end, W, H, fps)
    # red frame synthesized FROM the finite geo stream (drawbox t=fill) — an infinite
    # lavfi color input does not reliably terminate under -filter_complex
    fg = ("[0:v]split=3[g0_in][g1_in][g2_in];"
          "[g0_in]format=gbrp[g0];[g1_in]format=gbrp[g1];"
          "[g2_in]drawbox=x=0:y=0:w=iw:h=ih:color=red:t=fill,format=gbrp[red];"
          "[1:v]extractplanes=y,setrange=full,format=gbrp[m];"
          "[g1][red][m]maskedmerge[fr];"
          "[g0][fr]blend=all_mode=normal:all_opacity=0.5,format=yuv420p[v]")
    r = subprocess.run(
        [FF, "-y", "-i", str(geo), "-i", str(matte),
         "-filter_complex", fg, "-map", "[v]", "-map", "0:a?",
         "-c:v", "libx264", "-crf", "18", "-preset", "medium", "-c:a", "copy", str(out)],
        capture_output=True, text=True, timeout=900)
    if not Path(out).exists():
        raise RuntimeError("matte preview failed: " + (r.stderr or "")[-400:])
    return str(out)


def _mode_grade(mode):
    """Trailing grade of a mode name ('lux2-warm' -> 'warm'), default otherwise."""
    tail = mode.rsplit("-", 1)[-1]
    return tail if tail in GRADES else DEFAULT_GRADE


def render(video, start, end, title, out, aspect="9:16", mode="quiet", font=DIDOT):
    if mode == "matte-preview":           # QC: matte as 50% red overlay
        return render_matte_preview(video, start, end, out, aspect)
    if mode.startswith("lux2"):           # "lux2" | "lux2-cool" | "lux2-warm" | "lux2-natural"
        return render_lux2(video, start, end, out, aspect, _mode_grade(mode))
    if mode.startswith(("luxury-masked", "masked")):   # "luxury-masked-*" a.k.a. "masked-*"
        return render_masked(video, start, end, out, aspect, _mode_grade(mode))
    if mode.startswith("pure"):           # "pure" | "pure-cool" | "pure-warm" | "pure-natural"
        grade = mode.split("-", 1)[1] if "-" in mode else DEFAULT_GRADE
        return render_pure(video, start, end, out, aspect, grade)
    m = MODES.get(mode, MODES["quiet"])
    W, H = (1080, 1920) if aspect == "9:16" else (1080, 1080) if aspect == "1:1" else (1920, 1080)
    dur = round(float(end) - float(start), 2)
    crop = f"crop='min(iw,ih*{W}/{H})':'min(ih,iw*{H}/{W})':(iw-ow)/2:(ih-oh)/2,scale={W}:{H}"
    with tempfile.TemporaryDirectory() as td:
        ass = Path(td) / "lux.ass"
        ass.write_text(_ass(title, dur, W, H, m["cap_size"], m["cap_spacing"], m["cap_marginv"], font))
        # a short gold hairline above the caption; frame only in cinematic mode
        chain = [crop, m["grade"], m["bloom"], m["film"]]
        if m["frame"]:
            chain.append(f"drawbox=x=26:y=26:w=iw-52:h=ih-52:color={GOLD}@0.45:t=2")
        rule_y = int(H * (0.82 if not m["frame"] else 0.79))
        chain.append(f"drawbox=x=(iw-130)/2:y={rule_y}:w=130:h=2:color={GOLD}@0.8:t=fill")
        chain.append(f"ass={ass}")
        vf = ",".join(chain)
        r = subprocess.run(
            [FF, "-y", "-ss", str(start), "-i", str(video), "-t", str(dur),
             "-vf", vf, "-c:v", "libx264", "-crf", "18", "-preset", "slow",
             "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "192k", str(out)],
            capture_output=True, text=True, timeout=1200)
        if not Path(out).exists():
            raise RuntimeError("luxury render failed: " + (r.stderr or "")[-500:])
    return str(out)


if __name__ == "__main__":
    a = argparse.ArgumentParser()
    a.add_argument("--video", required=True); a.add_argument("--out", required=True)
    a.add_argument("--title", default=""); a.add_argument("--aspect", default="9:16")
    # quiet | cinematic | pure[-GRADE] | lux2[-GRADE] | luxury-masked-GRADE (a.k.a.
    # masked-GRADE) | matte-preview        GRADE = cool | warm | natural
    a.add_argument("--mode", default="quiet")
    a.add_argument("--font", default=DIDOT, help="caption family for quiet/cinematic (installed system font; default Didot)")
    a.add_argument("--grade", default="cool", choices=["cool", "warm", "natural"])
    a.add_argument("--lut", default=None, help="explicit .cube path (overrides grade)")
    a.add_argument("--start", type=float, default=0); a.add_argument("--end", type=float, default=30)
    p = a.parse_args()
    if p.mode.startswith("pure"):
        grade = p.mode.split("-", 1)[1] if "-" in p.mode else p.grade
        print(render_pure(p.video, p.start, p.end, p.out, p.aspect, grade, p.lut))
    elif p.mode.startswith("lux2"):
        grade = p.mode.split("-", 1)[1] if "-" in p.mode else p.grade
        print(render_lux2(p.video, p.start, p.end, p.out, p.aspect, grade, p.lut))
    elif p.mode.startswith(("luxury-masked", "masked")):
        grade = _mode_grade(p.mode) if "-" in p.mode else p.grade
        print(render_masked(p.video, p.start, p.end, p.out, p.aspect, grade, p.lut))
    elif p.mode == "matte-preview":
        print(render_matte_preview(p.video, p.start, p.end, p.out, p.aspect))
    else:
        print(render(p.video, p.start, p.end, p.title, p.out, p.aspect, p.mode, p.font))
