#!/usr/bin/env python3
"""Freeze-frame quote cards (HLC Instagram style).

Each card is 1080x1350 (IG 4:5): two stacked freeze-frames, a bold white/black-outline
quote burned at the bottom of each, a small "HIGH LVL CONVERSATIONS" watermark, and a red
brand $ mark at the seam. Single-frame cards (one quote) also supported.

Define cards below (timestamps in seconds into the source video) and run:
  <venv>/python quote_cards.py <source_video> <out_dir>
"""
import subprocess
import sys
import textwrap
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont

W = 1080
H = 1350
FFMPEG = "/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg"

FONT_CANDIDATES = [
    "/System/Library/Fonts/Supplemental/Arial Narrow Bold.ttf",
    "/System/Library/Fonts/Supplemental/Arial Bold.ttf",
    "/Library/Fonts/Arial Bold.ttf",
    "/System/Library/Fonts/HelveticaNeue.ttc",
]


def font(size):
    for p in FONT_CANDIDATES:
        if Path(p).exists():
            try:
                return ImageFont.truetype(p, size)
            except Exception:
                continue
    return ImageFont.load_default()


def grab(src, t, dst):
    subprocess.run([FFMPEG, "-y", "-ss", str(t), "-i", src, "-frames:v", "1", dst,
                    "-loglevel", "error"], check=True)


def cover(img, w, h):
    iw, ih = img.size
    s = max(w / iw, h / ih)
    img = img.resize((int(iw * s + 1), int(ih * s + 1)), Image.LANCZOS)
    iw, ih = img.size
    return img.crop(((iw - w) // 2, (ih - h) // 2, (iw - w) // 2 + w, (ih - h) // 2 + h))


def wrap_to_fit(draw, text, max_w, max_lines, hi=58, lo=34):
    """Pick the biggest font size where the uppercase text wraps to <= max_lines."""
    text = text.upper()
    for size in range(hi, lo - 1, -2):
        f = font(size)
        # greedy wrap by measured width
        words, lines, cur = text.split(), [], ""
        for wd in words:
            t = (cur + " " + wd).strip()
            if draw.textlength(t, font=f) <= max_w:
                cur = t
            else:
                lines.append(cur)
                cur = wd
        if cur:
            lines.append(cur)
        if len(lines) <= max_lines:
            return f, lines
    return font(lo), lines


def draw_quote(card, region, text):
    """Burn a quote onto the bottom of a frame region (x0,y0,x1,y1)."""
    x0, y0, x1, y1 = region
    rw = x1 - x0
    d = ImageDraw.Draw(card)
    f, lines = wrap_to_fit(d, f'“{text}”', rw - 120, 3)
    lh = f.size + 12
    block_h = lh * len(lines)
    # darken the bottom of the frame so text always reads
    grad = Image.new("L", (1, y1 - y0), 0)
    for i in range(y1 - y0):
        grad.putpixel((0, i), int(170 * max(0, (i - (y1 - y0) * 0.55) / ((y1 - y0) * 0.45))))
    shade = Image.new("RGBA", (rw, y1 - y0), (0, 0, 0, 255))
    shade.putalpha(grad.resize((rw, y1 - y0)))
    card.alpha_composite(shade, (x0, y0))
    d = ImageDraw.Draw(card)
    ty = y1 - block_h - 46
    for ln in lines:
        tw = d.textlength(ln, font=f)
        d.text((x0 + (rw - tw) / 2, ty), ln, font=f, fill=(255, 255, 255, 255),
               stroke_width=max(3, f.size // 14), stroke_fill=(0, 0, 0, 255))
        ty += lh
    # watermark bottom-left
    wf = font(22)
    d.text((x0 + 24, y1 - 64), "HIGH LVL", font=wf, fill=(255, 255, 255, 150))
    d.text((x0 + 24, y1 - 40), "CONVERSATIONS", font=wf, fill=(255, 255, 255, 150))


def brand_dollar(card, cy):
    """Red $ brand mark at the right edge, vertically centered at cy."""
    d = ImageDraw.Draw(card)
    f = font(120)
    red = (214, 40, 40, 255)
    d.text((W - 96, cy - 78), "$", font=f, fill=red, stroke_width=4, stroke_fill=(120, 0, 0, 255))
    # up/down arrow accents
    d.polygon([(W - 64, cy - 96), (W - 78, cy - 76), (W - 50, cy - 76)], fill=red)
    d.polygon([(W - 64, cy + 96), (W - 78, cy + 76), (W - 50, cy + 76)], fill=red)


def make_card(src, out, top, bottom=None):
    """top/bottom = (timestamp, quote). If bottom is None, single full-frame card."""
    card = Image.new("RGBA", (W, H), (0, 0, 0, 255))
    tmp = Path("/tmp/qcards"); tmp.mkdir(exist_ok=True)
    if bottom:
        fh = H // 2
        for idx, (t, q) in enumerate([top, bottom]):
            fp = str(tmp / f"f{idx}.png")
            grab(src, t, fp)
            frame = cover(Image.open(fp).convert("RGBA"), W, fh)
            card.alpha_composite(frame, (0, idx * fh))
            draw_quote(card, (0, idx * fh, W, idx * fh + fh), q)
        ImageDraw.Draw(card).line([(0, fh), (W, fh)], fill=(255, 255, 255, 60), width=2)
        brand_dollar(card, fh)
    else:
        t, q = top
        fp = str(tmp / "f0.png")
        grab(src, t, fp)
        card.alpha_composite(cover(Image.open(fp).convert("RGBA"), W, H), (0, 0))
        draw_quote(card, (0, 0, W, H), q)
        brand_dollar(card, H - 200)
    card.convert("RGB").save(out, quality=95)
    print("saved", out)


# ---- card definitions (timestamp seconds, quote) ----
CARDS = [
    ("card-1-money", ("double",),
     (1090, "It's harder to keep money than it is to make it."),
     (3004, "True wealth is being happy with what you have.")),
    ("card-2-consumer", ("double",),
     (3114, "Spending power is just a signal that you're a consumer."),
     (5262, "The point of wealth is not to compete — it's to coordinate.")),
    ("card-3-sovereignty", ("double",),
     (2831, "Generational wealth needs to become generational sovereignty."),
     (2798, "The family becomes its own bank — its own private government.")),
    ("card-4-wisdom", ("double",),
     (1715, "You can make money early and be dumb as hell."),
     (1957, "Discipline is the tax on misalignment.")),
    ("card-5-family", ("double",),
     (303, "We've lost the plot on what family is for."),
     (1662, "Afraid to start a family is afraid to continue the future.")),
    ("card-6-meaning", ("single",),
     (1917, "Are you making meaning, or are you making money?"), None),
]

if __name__ == "__main__":
    src, out_dir = sys.argv[1], sys.argv[2]
    Path(out_dir).mkdir(parents=True, exist_ok=True)
    for name, kind, top, bottom in CARDS:
        make_card(src, f"{out_dir}/{name}.png", top, bottom)
