#!/usr/bin/env python3
"""batch_cards.py — render a ranked quote-bank into HIGH LVL cards (conversation + quote + xpost).

Input: a bank JSON (list, or {"kept":[...]}) of items with:
  refined_quote, refined_headline, start_s, end_s, speaker ("19keys"|"guest"|"unclear")
For each of the top N it renders the requested styles off the episode source, deriving:
  - conversation: head=headline, speaker label, trans=verbatim transcript for the span w/ auto-gold keywords
  - quote:        the quote over the full wide frame (16:9, no crop)
  - xpost:        the quote as a tweet card w/ 1-2 red-underlined keywords

Usage:
  python3 tools/batch_cards.py --bank bank.json --source <mp4> --words words.json \
      --outdir ~/Movies/HLC-clips/reels/eiw-batch --top 20 --styles conversation,quote,xpost
"""
import argparse
import json
import re
import subprocess
from pathlib import Path

import style_render as sr

FFFULL = "/opt/homebrew/opt/ffmpeg-full/bin"


def make_thumb(mp4, thumbs_dir):
    """One poster WebP per clip so the Library gallery shows a real frame instantly (matches server.make_thumb)."""
    try:
        thumbs_dir.mkdir(parents=True, exist_ok=True)
        t = thumbs_dir / (Path(mp4).stem + ".webp")
        subprocess.run([f"{FFFULL}/ffmpeg", "-y", "-ss", "0.5", "-i", str(mp4), "-frames:v", "1",
                        "-vf", "scale=480:-2", "-c:v", "libwebp", "-quality", "72", str(t)],
                       capture_output=True, timeout=30)
    except Exception:
        pass

STOP = set("the a an and or but so of to in on at for with your you we they it is are was were be been "
           "that this these those i he she them his her our my me if as by from not no do does did have has "
           "had will would can could should about into than then there here what who how why when where all "
           "just like right um mm yeah".split())


def load_words(words_path):
    d = json.loads(Path(words_path).read_text())
    w = d["words"] if isinstance(d, dict) else d
    return [x for x in w if x.get("type", "word") == "word"]


def verbatim(words, a, b):
    span = [x for x in words if a - 0.05 <= x["start"] <= b + 0.05]
    return " ".join(x["text"] for x in span).strip()


def keywords(text, n=2):
    """Pick the n most salient words (longest, non-stopword) for highlighting."""
    seen, picks = set(), []
    for w in sorted(re.findall(r"[A-Za-z][A-Za-z'-]+", text), key=len, reverse=True):
        lw = w.lower()
        if lw in STOP or lw in seen or len(w) < 4:
            continue
        seen.add(lw)
        picks.append(w)
        if len(picks) >= n:
            break
    return picks


def mark(text, words_to_mark, open_t, close_t):
    """Wrap the first occurrence of each word (whole-word, case-insensitive) in open_t/close_t."""
    out = text
    for w in words_to_mark:
        out = re.sub(rf"(?<![\w]){re.escape(w)}(?![\w])", f"{open_t}{w}{close_t}", out, count=1)
    return out


SPEAKER_LABEL = {"19keys": "19 KEYS", "guest": "BOYCE WATKINS", "unclear": "19 KEYS"}


def render_one(item, idx, source, words, outdir, styles):
    a, b = float(item["start_s"]), float(item["end_s"])
    quote = item["refined_quote"].strip().strip('"').strip()
    head = item["refined_headline"].strip().strip('"').strip()
    speaker = SPEAKER_LABEL.get(str(item.get("speaker", "")).lower(), "19 KEYS")
    made = {}
    for style in styles:
        out = Path(outdir) / f"eiw-q{idx:02d}-{style}.mp4"
        if style == "conversation":
            tr = verbatim(words, a, b) or quote
            tr = mark(tr, keywords(head, 2), "[g]", "[/g]")
            params = {"head": head, "speaker": speaker, "trans": tr}
        elif style == "quote":
            params = {"quote": quote, "aspect": "wide"}
        elif style == "xpost":
            # card shows ONLY the one-liner hook (card_line: pre-picked verbatim line w/ [u] marks +
            # signature already applied by speaker) — the CLIP runs the full riff independently.
            if item.get("card_line"):
                post = item["card_line"]
            else:  # fallback: derive from the quote; sign only 19Keys' own words
                sig = " -19Keys" if str(item.get("speaker", "")).lower() == "19keys" else ""
                post = mark(quote, keywords(quote, 2), "[u]", "[/u]") + sig
            params = {"name": "19keys", "at": "@19keys_", "post": post}
        else:
            continue
        try:
            sr.render(source, a, b, style, str(out), params=params, captions="off")
            make_thumb(out, Path(outdir).parent / ".thumbs")   # poster so it shows in the Library gallery
            made[style] = str(out)
        except Exception as e:  # keep the batch going; report the failure
            made[style] = f"FAILED: {e}"
    return made


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--bank", required=True)
    ap.add_argument("--source", required=True)
    ap.add_argument("--words", required=True)
    ap.add_argument("--outdir", required=True)
    ap.add_argument("--top", type=int, default=20)
    ap.add_argument("--offset", type=int, default=0, help="skip the first N (for parallel workers)")
    ap.add_argument("--count", type=int, default=0, help="render only this many after offset (0 = through --top)")
    ap.add_argument("--styles", default="conversation,quote,xpost")
    a = ap.parse_args()

    bank = json.loads(Path(a.bank).read_text())
    items = bank["kept"] if isinstance(bank, dict) and "kept" in bank else bank
    items = items[: a.top]
    lo = a.offset
    hi = (a.offset + a.count) if a.count else len(items)
    styles = [s.strip() for s in a.styles.split(",") if s.strip()]
    words = load_words(a.words)
    Path(a.outdir).mkdir(parents=True, exist_ok=True)

    manifest = []
    for i in range(lo, min(hi, len(items))):
        it = items[i]
        made = render_one(it, i + 1, a.source, words, a.outdir, styles)   # i+1 = global rank
        row = {"idx": i + 1, "headline": it["refined_headline"], "span": [it["start_s"], it["end_s"]],
               "score": it.get("final_score"), "renders": made}
        manifest.append(row)
        print(f"[{i+1}/{len(items)}] {it['refined_headline']}  -> {', '.join(k for k,v in made.items() if not str(v).startswith('FAILED'))}")
    Path(a.outdir, f"manifest-{lo:02d}.json").write_text(json.dumps(manifest, indent=2))  # offset-scoped: parallel-safe
    print("manifest:", str(Path(a.outdir, f"manifest-{lo:02d}.json")))


if __name__ == "__main__":
    main()
