#!/usr/bin/env python3
"""Upload podcast clips to social media (direct YouTube & Bluesky, Postiz for others).

Usage:
    python upload_clips.py                          # interactive: pick episode, clips, platforms
    python upload_clips.py clips/episode-12/        # pick clips and platforms interactively
    python upload_clips.py clips/episode-12/ --clip 1 --platforms ig,yt
    python upload_clips.py clips/episode-12/ --yes  # skip all prompts, upload everything
"""

import argparse
import json
import re
import sys
from pathlib import Path

import requests
from atproto import Client as BskyClient
from dotenv import load_dotenv
import os

load_dotenv(Path(__file__).parent / ".env")

POSTIZ_API_KEY = os.getenv("POSTIZ_API_KEY")
POSTIZ_URL = os.getenv("POSTIZ_URL", "https://social.lukeattheroost.com")
POSTIZ_INTEGRATIONS = json.loads(os.getenv("POSTIZ_INTEGRATIONS", "{}"))

BSKY_HANDLE = os.getenv("BSKY_HANDLE", "lukeattheroost.bsky.social")
BSKY_APP_PASSWORD = os.getenv("BSKY_APP_PASSWORD")

YT_CLIENT_SECRETS = Path(__file__).parent / "youtube_client_secrets.json"
YT_TOKEN_FILE = Path(__file__).parent / "youtube_token.json"

PLATFORM_ALIASES = {
    "ig": "instagram", "insta": "instagram", "instagram": "instagram",
    "yt": "youtube", "youtube": "youtube",
    "fb": "facebook", "facebook": "facebook",
    "bsky": "bluesky", "bluesky": "bluesky",
    "masto": "mastodon", "mastodon": "mastodon",
    "nostr": "nostr",
    "li": "linkedin", "linkedin": "linkedin",
    "threads": "threads",
    "tt": "tiktok", "tiktok": "tiktok",
}

PLATFORM_DISPLAY = {
    "instagram": "Instagram Reels",
    "youtube": "YouTube Shorts",
    "facebook": "Facebook Reels",
    "bluesky": "Bluesky",
    "mastodon": "Mastodon",
    "nostr": "Nostr",
    "linkedin": "LinkedIn",
    "threads": "Threads",
    "tiktok": "TikTok",
}

ALL_PLATFORMS = list(PLATFORM_DISPLAY.keys())

UPLOAD_LEDGER_FILE = "upload-history.json"


def load_upload_history(clips_dir: Path) -> dict:
    """Load upload history for a clips directory.
    Returns dict mapping clip_file -> list of platforms already uploaded to.
    """
    ledger = clips_dir / UPLOAD_LEDGER_FILE
    if ledger.exists():
        with open(ledger) as f:
            return json.load(f)
    return {}


def save_upload_history(clips_dir: Path, history: dict):
    with open(clips_dir / UPLOAD_LEDGER_FILE, "w") as f:
        json.dump(history, f, indent=2)


def get_api_url(path: str) -> str:
    base = POSTIZ_URL.rstrip("/")
    return f"{base}/api/public/v1{path}"


def api_headers() -> dict:
    return {
        "Authorization": POSTIZ_API_KEY,
        "Content-Type": "application/json",
    }


def fetch_integrations() -> list[dict]:
    resp = requests.get(get_api_url("/integrations"), headers=api_headers(), timeout=15)
    if resp.status_code != 200:
        print(f"Error fetching integrations: {resp.status_code} {resp.text[:200]}")
        sys.exit(1)
    return resp.json()


BLOCKED_INTEGRATION_IDS = {
    "cmluam50j0001o46xifujx059",  # Personal LinkedIn (CareerPulse) — never post podcast content here
}

def find_integration(integrations: list[dict], provider: str) -> dict | None:
    # Prefer hardcoded integration ID from .env (avoids picking wrong account)
    if provider in POSTIZ_INTEGRATIONS:
        target_id = POSTIZ_INTEGRATIONS[provider].get("id")
        if target_id:
            for integ in integrations:
                if integ.get("id") == target_id:
                    return integ
    # Fallback: first matching provider (skip blocked accounts)
    for integ in integrations:
        if integ.get("id") in BLOCKED_INTEGRATION_IDS:
            continue
        if integ.get("identifier", "").startswith(provider) and not integ.get("disabled"):
            return integ
    return None


def upload_file(file_path: Path) -> dict:
    headers = {"Authorization": POSTIZ_API_KEY}
    with open(file_path, "rb") as f:
        resp = requests.post(
            get_api_url("/upload"),
            headers=headers,
            files={"file": (file_path.name, f, "video/mp4")},
            timeout=120,
        )
    if resp.status_code not in (200, 201):
        print(f"Upload failed: {resp.status_code} {resp.text[:200]}")
        return {}
    return resp.json()


def build_content(clip: dict, platform: str) -> str:
    desc = clip.get("description", clip.get("caption_text", ""))
    hashtags = clip.get("hashtags", [])
    hashtag_str = " ".join(hashtags)

    if platform == "bluesky":
        if hashtags and len(desc) + 2 + len(hashtag_str) <= 300:
            return desc + "\n\n" + hashtag_str
        return desc[:300]

    parts = [desc]
    if hashtags:
        parts.append("\n\n" + hashtag_str)
    if platform in ("youtube", "facebook"):
        parts.append("\n\nListen to the full episode: lukeattheroost.com")
    return "".join(parts)


def build_settings(clip: dict, platform: str) -> dict:
    if platform == "instagram":
        return {"__type": "instagram", "post_type": "post", "collaborators": []}
    if platform == "youtube":
        yt_tags = [{"value": h.lstrip("#"), "label": h.lstrip("#")}
                   for h in clip.get("hashtags", [])]
        return {
            "__type": "youtube",
            "title": clip["title"],
            "type": "public",
            "selfDeclaredMadeForKids": "no",
            "thumbnail": None,
            "tags": yt_tags,
        }
    if platform == "tiktok":
        return {
            "__type": "tiktok",
            "privacy_level": "PUBLIC_TO_EVERYONE",
            "duet": False,
            "stitch": False,
            "comment": True,
            "autoAddMusic": "no",
            "brand_content_toggle": False,
            "brand_organic_toggle": False,
            "content_posting_method": "DIRECT_POST",
        }
    return {"__type": platform}


def post_to_bluesky(clip: dict, clip_file: Path) -> bool:
    """Post a clip directly to Bluesky via atproto (bypasses Postiz)."""
    import time
    import httpx
    from atproto import models

    if not BSKY_APP_PASSWORD:
        print("    Error: BSKY_APP_PASSWORD not set in .env")
        return False

    client = BskyClient()
    client.login(BSKY_HANDLE, BSKY_APP_PASSWORD)
    did = client.me.did
    video_data = clip_file.read_bytes()

    # Get a service auth token scoped to the user's PDS (required by video service)
    from urllib.parse import urlparse
    pds_host = urlparse(client._session.pds_endpoint).hostname
    service_auth = client.com.atproto.server.get_service_auth(
        {"aud": f"did:web:{pds_host}", "lxm": "com.atproto.repo.uploadBlob"}
    )
    token = service_auth.token

    # Upload video to Bluesky's video processing service (not the PDS)
    print(f"    Uploading video ({len(video_data) / 1_000_000:.1f} MB)...")
    upload_resp = httpx.post(
        "https://video.bsky.app/xrpc/app.bsky.video.uploadVideo",
        params={"did": did, "name": clip_file.name},
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "video/mp4",
        },
        content=video_data,
        timeout=120,
    )
    if upload_resp.status_code not in (200, 409):
        print(f"    Upload failed: {upload_resp.status_code} {upload_resp.text[:200]}")
        return False

    upload_data = upload_resp.json()
    job_id = upload_data.get("jobId") or upload_data.get("jobStatus", {}).get("jobId")
    if not job_id:
        print(f"    No jobId returned: {upload_resp.text[:200]}")
        return False
    print(f"    Video processing (job {job_id})...")

    # Poll until video is processed
    session_token = client._session.access_jwt
    blob = None
    while True:
        status_resp = httpx.get(
            "https://video.bsky.app/xrpc/app.bsky.video.getJobStatus",
            params={"jobId": job_id},
            headers={"Authorization": f"Bearer {session_token}"},
            timeout=15,
        )
        resp_data = status_resp.json()
        status = resp_data.get("jobStatus") or resp_data
        state = status.get("state")
        if state == "JOB_STATE_COMPLETED":
            blob = status.get("blob")
            break
        if state == "JOB_STATE_FAILED":
            err = status.get("error") or status.get("message") or "unknown"
            print(f"    Video processing failed: {err}")
            return False
        progress = status.get("progress", 0)
        print(f"    Processing... {progress}%")
        time.sleep(3)

    if not blob:
        print("    No blob returned after processing")
        return False

    text = build_content(clip, "bluesky")

    embed = models.AppBskyEmbedVideo.Main(
        video=models.blob_ref.BlobRef(
            mime_type=blob["mimeType"],
            size=blob["size"],
            ref=models.blob_ref.IpldLink(link=blob["ref"]["$link"]),
        ),
        alt=clip.get("caption_text", clip["title"]),
        aspect_ratio=models.AppBskyEmbedDefs.AspectRatio(width=1080, height=1920),
    )
    client.send_post(text=text, embed=embed)
    return True


def get_youtube_service():
    """Authenticate with YouTube API. First run opens a browser, then reuses saved token."""
    from google.oauth2.credentials import Credentials
    from google_auth_oauthlib.flow import InstalledAppFlow
    from google.auth.transport.requests import Request
    from googleapiclient.discovery import build as yt_build

    scopes = ["https://www.googleapis.com/auth/youtube.upload"]
    creds = None

    if YT_TOKEN_FILE.exists():
        creds = Credentials.from_authorized_user_file(str(YT_TOKEN_FILE), scopes)

    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            if not YT_CLIENT_SECRETS.exists():
                print("    Error: youtube_client_secrets.json not found")
                print("    Download OAuth2 Desktop App credentials from Google Cloud Console")
                return None
            flow = InstalledAppFlow.from_client_secrets_file(str(YT_CLIENT_SECRETS), scopes)
            creds = flow.run_local_server(port=8090)

        with open(YT_TOKEN_FILE, "w") as f:
            f.write(creds.to_json())

    return yt_build("youtube", "v3", credentials=creds)


def post_to_youtube(clip: dict, clip_file: Path) -> bool:
    """Upload a clip directly to YouTube Shorts via the Data API."""
    import time
    import random
    from googleapiclient.http import MediaFileUpload
    from googleapiclient.errors import HttpError

    youtube = get_youtube_service()
    if not youtube:
        return False

    title = clip["title"]
    if "#Shorts" not in title:
        title = f"{title} #Shorts"

    description = build_content(clip, "youtube")
    if "#Shorts" not in description:
        description += "\n\n#Shorts"

    tags = [h.lstrip("#") for h in clip.get("hashtags", [])]
    if "Shorts" not in tags:
        tags.insert(0, "Shorts")

    body = {
        "snippet": {
            "title": title[:100],
            "description": description,
            "tags": tags,
            "categoryId": "24",  # Entertainment
        },
        "status": {
            "privacyStatus": "public",
            "selfDeclaredMadeForKids": False,
        },
    }

    media = MediaFileUpload(
        str(clip_file),
        mimetype="video/mp4",
        chunksize=256 * 1024,
        resumable=True,
    )

    request = youtube.videos().insert(part="snippet,status", body=body, media_body=media)

    file_size = clip_file.stat().st_size / 1_000_000
    print(f"    Uploading video ({file_size:.1f} MB)...")

    response = None
    retry = 0
    while response is None:
        try:
            status, response = request.next_chunk()
            if status:
                print(f"    Upload {int(status.progress() * 100)}%...")
        except HttpError as e:
            if e.resp.status in (500, 502, 503, 504) and retry < 5:
                retry += 1
                wait = random.random() * (2 ** retry)
                print(f"    Retrying in {wait:.1f}s...")
                time.sleep(wait)
            else:
                print(f"    YouTube API error: {e}")
                return False

    video_id = response["id"]
    print(f"    https://youtube.com/shorts/{video_id}")
    return video_id


def create_post(integration_id: str, content: str, media: dict,
                settings: dict, schedule: str | None = None) -> dict:
    from datetime import datetime, timezone
    post_type = "schedule" if schedule else "now"
    date = schedule or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
    payload = {
        "type": post_type,
        "date": date,
        "shortLink": False,
        "tags": [],
        "posts": [
            {
                "integration": {"id": integration_id},
                "value": [
                    {
                        "content": content,
                        "image": [media] if media else [],
                    }
                ],
                "settings": settings,
            }
        ],
    }

    resp = requests.post(
        get_api_url("/posts"),
        headers=api_headers(),
        json=payload,
        timeout=30,
    )
    if resp.status_code not in (200, 201):
        print(f"Post creation failed: {resp.status_code} {resp.text[:300]}")
        return {}
    return resp.json()


def main():
    valid_names = sorted(set(PLATFORM_ALIASES.keys()))
    parser = argparse.ArgumentParser(description="Upload podcast clips to social media via Postiz")
    parser.add_argument("clips_dir", nargs="?", help="Path to clips directory (e.g. clips/episode-12/). If omitted, shows a picker.")
    parser.add_argument("--clip", "-c", type=int, help="Upload only clip N (1-indexed)")
    parser.add_argument("--platforms", "-p",
                        help=f"Comma-separated platforms ({','.join(ALL_PLATFORMS)}). Default: all")
    parser.add_argument("--schedule", "-s", help="Schedule time (ISO 8601, e.g. 2026-02-16T10:00:00)")
    parser.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt")
    parser.add_argument("--dry-run", action="store_true", help="Show what would be uploaded without posting")
    args = parser.parse_args()

    if not POSTIZ_API_KEY:
        print("Error: POSTIZ_API_KEY not set in .env")
        sys.exit(1)

    # Resolve clips directory — pick interactively if not provided
    if args.clips_dir:
        clips_dir = Path(args.clips_dir).expanduser().resolve()
    else:
        clips_root = Path(__file__).parent / "clips"
        episode_dirs = sorted(
            [d for d in clips_root.iterdir()
             if d.is_dir() and not d.name.startswith(".") and (d / "clips-metadata.json").exists()],
            key=lambda d: (int(m.group(1)) if (m := re.search(r'(\d+)', d.name)) else 0, d.name),
        )
        if not episode_dirs:
            print("No clip directories found in clips/. Run make_clips.py first.")
            sys.exit(1)
        print("\nAvailable episodes:\n")
        for i, d in enumerate(episode_dirs):
            with open(d / "clips-metadata.json") as f:
                meta = json.load(f)
            print(f"  {i+1}. {d.name} ({len(meta)} clip{'s' if len(meta) != 1 else ''})")
        print()
        while True:
            try:
                choice = input("Which episode? ").strip()
                idx = int(choice) - 1
                if 0 <= idx < len(episode_dirs):
                    clips_dir = episode_dirs[idx]
                    break
                print(f"  Enter 1-{len(episode_dirs)}")
            except (ValueError, EOFError):
                print(f"  Enter an episode number")

    metadata_path = clips_dir / "clips-metadata.json"
    if not metadata_path.exists():
        print(f"Error: No clips-metadata.json found in {clips_dir}")
        print("Run make_clips.py first to generate clips and metadata.")
        sys.exit(1)

    with open(metadata_path) as f:
        clips = json.load(f)

    # Pick clips
    if args.clip:
        if args.clip < 1 or args.clip > len(clips):
            print(f"Error: Clip {args.clip} not found (have {len(clips)} clips)")
            sys.exit(1)
        clips = [clips[args.clip - 1]]
    elif not args.yes:
        print(f"\nFound {len(clips)} clip(s):\n")
        for i, clip in enumerate(clips):
            desc = clip.get('description', clip.get('caption_text', ''))
            if len(desc) > 70:
                desc = desc[:desc.rfind(' ', 0, 70)] + '...'
            print(f"  {i+1}. \"{clip['title']}\" ({clip['duration']:.0f}s)")
            print(f"     {desc}")
        print(f"\n  a. All clips")
        print()
        while True:
            choice = input("Which clips? (e.g. 1,3 or a for all): ").strip().lower()
            if choice in ('a', 'all'):
                break
            try:
                indices = [int(x.strip()) for x in choice.split(",")]
                if all(1 <= x <= len(clips) for x in indices):
                    clips = [clips[x - 1] for x in indices]
                    break
                print(f"  Invalid selection. Enter 1-{len(clips)}, comma-separated, or 'a' for all.")
            except (ValueError, EOFError):
                print(f"  Enter clip numbers (e.g. 1,3) or 'a' for all")

    # Pick platforms
    if args.platforms:
        requested = []
        for p in args.platforms.split(","):
            p = p.strip().lower()
            if p not in PLATFORM_ALIASES:
                print(f"Unknown platform: {p}")
                print(f"Valid: {', '.join(valid_names)}")
                sys.exit(1)
            requested.append(PLATFORM_ALIASES[p])
        target_platforms = list(dict.fromkeys(requested))
    elif not args.yes:
        print(f"\nPlatforms:\n")
        for i, p in enumerate(ALL_PLATFORMS):
            print(f"  {i+1}. {PLATFORM_DISPLAY[p]}")
        print(f"\n  a. All platforms (default)")
        print()
        choice = input("Which platforms? (e.g. 1,3,5 or a for all) [a]: ").strip().lower()
        if choice and choice not in ('a', 'all'):
            try:
                indices = [int(x.strip()) for x in choice.split(",")]
                target_platforms = [ALL_PLATFORMS[x - 1] for x in indices if 1 <= x <= len(ALL_PLATFORMS)]
                if not target_platforms:
                    target_platforms = ALL_PLATFORMS[:]
            except (ValueError, IndexError):
                target_platforms = ALL_PLATFORMS[:]
        else:
            target_platforms = ALL_PLATFORMS[:]
    else:
        target_platforms = ALL_PLATFORMS[:]

    DIRECT_PLATFORMS = {"bluesky", "youtube"}
    needs_postiz = not args.dry_run and any(
        p not in DIRECT_PLATFORMS for p in target_platforms)
    if needs_postiz:
        print("Fetching connected accounts from Postiz...")
        integrations = fetch_integrations()
    else:
        integrations = []

    active_platforms = {}
    for platform in target_platforms:
        if platform == "bluesky":
            if BSKY_APP_PASSWORD or args.dry_run:
                active_platforms[platform] = {"name": BSKY_HANDLE, "_direct": True}
            else:
                print("Warning: BSKY_APP_PASSWORD not set in .env, skipping Bluesky")
            continue
        if platform == "youtube":
            if YT_CLIENT_SECRETS.exists() or YT_TOKEN_FILE.exists() or args.dry_run:
                active_platforms[platform] = {"name": "YouTube Shorts", "_direct": True}
            else:
                print("Warning: youtube_client_secrets.json not found, skipping YouTube")
            continue
        if args.dry_run:
            active_platforms[platform] = {"name": PLATFORM_DISPLAY[platform]}
            continue
        integ = find_integration(integrations, platform)
        if integ:
            active_platforms[platform] = integ
        else:
            print(f"Warning: No {PLATFORM_DISPLAY[platform]} account connected in Postiz")

    if not args.dry_run and not active_platforms:
        print("Error: No platforms available to upload to")
        sys.exit(1)

    platform_names = [f"{PLATFORM_DISPLAY[p]} ({integ.get('name', 'connected')})"
                      for p, integ in active_platforms.items()]

    print(f"\nUploading {len(clips)} clip(s) to: {', '.join(platform_names)}")
    if args.schedule:
        print(f"Scheduled for: {args.schedule}")
    print()

    for i, clip in enumerate(clips):
        print(f"  {i+1}. \"{clip['title']}\" ({clip['duration']:.0f}s)")
        desc = clip.get('description', '')
        if len(desc) > 80:
            desc = desc[:desc.rfind(' ', 0, 80)] + '...'
        print(f"     {desc}")
        print(f"     {' '.join(clip.get('hashtags', []))}")
        print()

    if args.dry_run:
        print("Dry run — nothing uploaded.")
        return

    if not args.yes:
        confirm = input("Proceed? [y/N] ").strip().lower()
        if confirm != "y":
            print("Cancelled.")
            return

    upload_history = load_upload_history(clips_dir)

    for i, clip in enumerate(clips):
        clip_file = clips_dir / clip["clip_file"]
        if not clip_file.exists():
            print(f"  Clip {i+1}: Video file not found: {clip_file}")
            continue

        clip_key = clip["clip_file"]
        already_uploaded = set(upload_history.get(clip_key, []))
        remaining_platforms = {p: integ for p, integ in active_platforms.items()
                               if p not in already_uploaded}

        if not remaining_platforms:
            print(f"\n  Clip {i+1}: \"{clip['title']}\" — already uploaded to all selected platforms, skipping")
            continue

        skipped = already_uploaded & set(active_platforms.keys())
        if skipped:
            print(f"\n  Clip {i+1}: \"{clip['title']}\" (skipping already uploaded: {', '.join(sorted(skipped))})")
        else:
            print(f"\n  Clip {i+1}: \"{clip['title']}\"")

        postiz_platforms = {p: integ for p, integ in remaining_platforms.items()
                            if not integ.get("_direct")}

        media = None
        if postiz_platforms:
            print(f"    Uploading {clip_file.name}...")
            media = upload_file(clip_file)
            if not media:
                print("    Failed to upload video to Postiz, skipping Postiz platforms")
                postiz_platforms = {}
            else:
                print(f"    Uploaded: {media.get('path', 'ok')}")

        for platform, integ in postiz_platforms.items():
            display = PLATFORM_DISPLAY[platform]
            print(f"    Posting to {display}...")
            content = build_content(clip, platform)
            settings = build_settings(clip, platform)
            result = create_post(integ["id"], content, media, settings, args.schedule)
            if result:
                print(f"    {display}: Posted!")
                upload_history.setdefault(clip_key, []).append(platform)
                save_upload_history(clips_dir, upload_history)
            else:
                print(f"    {display}: Failed")

        if "youtube" in remaining_platforms:
            print(f"    Posting to YouTube Shorts (direct)...")
            try:
                yt_video_id = post_to_youtube(clip, clip_file)
                if yt_video_id:
                    print(f"    YouTube: Posted!")
                    upload_history.setdefault(clip_key, []).append("youtube")
                    save_upload_history(clips_dir, upload_history)
                else:
                    print(f"    YouTube: Failed")
            except Exception as e:
                print(f"    YouTube: Failed — {e}")

        if "bluesky" in remaining_platforms:
            print(f"    Posting to Bluesky (direct)...")
            try:
                if post_to_bluesky(clip, clip_file):
                    print(f"    Bluesky: Posted!")
                    upload_history.setdefault(clip_key, []).append("bluesky")
                    save_upload_history(clips_dir, upload_history)
                else:
                    print(f"    Bluesky: Failed")
            except Exception as e:
                print(f"    Bluesky: Failed — {e}")

    # Sync clips to website if any YouTube uploads happened
    if "youtube" in active_platforms:
        sync_clips_to_website()

    print("\nDone!")


WEBSITE_DIR = Path(__file__).parent / "website"
CLIPS_JSON = WEBSITE_DIR / "data" / "clips.json"
THUMBS_DIR = WEBSITE_DIR / "images" / "clips"
CLIPS_ROOT = Path(__file__).parent / "clips"


def sync_clips_to_website():
    """Rebuild website/data/clips.json from YouTube shorts and deploy."""
    import subprocess

    print("\nSyncing clips to website...")

    # Fetch all YouTube shorts from channel
    result = subprocess.run(
        ["python3", "-m", "yt_dlp", "--flat-playlist", "--print", "%(id)s\t%(title)s",
         "https://www.youtube.com/lukemacneil/shorts"],
        capture_output=True, text=True, timeout=60,
    )
    if not result.stdout.strip():
        print("  Could not fetch YouTube shorts, skipping sync")
        return

    yt_shorts = {}
    for line in result.stdout.strip().split("\n"):
        if "\t" not in line:
            continue
        vid_id, title = line.split("\t", 1)
        clean_title = re.sub(r"\s*#Shorts\s*$", "", title).strip().lower()
        yt_shorts[clean_title] = vid_id

    print(f"  Found {len(yt_shorts)} YouTube shorts")

    # Load all clip metadata and match to YouTube
    existing = {}
    if CLIPS_JSON.exists():
        for c in json.loads(CLIPS_JSON.read_text()):
            existing[c["clip_file"]] = c

    new_clips = []
    for ep_dir in sorted(CLIPS_ROOT.glob("episode-*")):
        meta_file = ep_dir / "clips-metadata.json"
        if not meta_file.exists():
            continue
        for clip in json.loads(meta_file.read_text()):
            clip_title = clip["title"].strip().lower()
            yt_id = yt_shorts.get(clip_title, "")
            if not yt_id:
                for yt_title, yid in yt_shorts.items():
                    if yt_title in clip_title or clip_title in yt_title:
                        yt_id = yid
                        break
            if not yt_id:
                continue

            prev = existing.get(clip["clip_file"], {})
            new_clips.append({
                "title": clip["title"],
                "description": clip.get("description", clip.get("caption_text", "")),
                "episode_number": clip.get("episode_number", 0),
                "clip_file": clip["clip_file"],
                "youtube_id": yt_id,
                "featured": prev.get("featured", False),
                "thumbnail": prev.get("thumbnail", ""),
            })

    new_clips.sort(key=lambda c: c["episode_number"], reverse=True)

    # Generate thumbnails for clips that don't have one
    THUMBS_DIR.mkdir(parents=True, exist_ok=True)
    for clip in new_clips:
        if clip["thumbnail"]:
            thumb_path = WEBSITE_DIR / clip["thumbnail"]
            if thumb_path.exists():
                continue

        thumb_name = clip["clip_file"].replace(".mp4", ".jpg")
        thumb_path = THUMBS_DIR / thumb_name
        ep_num = clip["episode_number"]
        mp4_path = CLIPS_ROOT / f"episode-{ep_num}" / clip["clip_file"]

        if not mp4_path.exists():
            continue

        subprocess.run(
            ["ffmpeg", "-y", "-i", str(mp4_path), "-ss", "3", "-vframes", "1",
             "-update", "1", "-vf", "scale=360:-2", "-q:v", "4", str(thumb_path)],
            capture_output=True, timeout=30,
        )
        if thumb_path.exists():
            clip["thumbnail"] = f"images/clips/{thumb_name}"
            print(f"  Generated thumbnail: {thumb_name}")

    # Ensure at least 3 featured
    featured_count = sum(1 for c in new_clips if c.get("featured"))
    if featured_count < 3:
        for c in new_clips:
            if not c.get("featured"):
                c["featured"] = True
                featured_count += 1
                if featured_count >= 3:
                    break

    CLIPS_JSON.parent.mkdir(parents=True, exist_ok=True)
    CLIPS_JSON.write_text(json.dumps(new_clips, indent=2))
    print(f"  Updated clips.json: {len(new_clips)} clips")

    # Deploy
    print("  Deploying website...")
    deploy = subprocess.run(
        ["npx", "wrangler", "pages", "deploy", "website/",
         "--project-name=lukeattheroost", "--branch=main", "--commit-dirty=true"],
        capture_output=True, text=True, timeout=120,
        cwd=str(Path(__file__).parent),
    )
    if "Deployment complete" in deploy.stdout:
        print("  Website deployed!")
    else:
        print(f"  Deploy failed: {deploy.stderr[-300:]}")


if __name__ == "__main__":
    main()
