#!/usr/bin/env python3
"""Grab a YouTube transcript and save as markdown.

Usage:
    python grab-transcript.py <youtube-url-or-id>
    python grab-transcript.py <url> --lang es
    python grab-transcript.py <url> --cookies /path/to/cookies.txt

Requires: yt-dlp (looks in venv at ~/.openclaw/workspace/.venv/bin/yt-dlp)

Cookies: Defaults to ~/.openclaw/workspace/cookies.txt
  These must be exported from a real browser (Netscape format) since
  YouTube blocks cloud server IPs. Re-export if they expire.

Output: ~/.openclaw/workspace/youtube-transcripts/<video-id>.md

Technical note:
  Uses -f sb0 (storyboard format) to work around YouTube's JS challenge
  solver requirement. The VPS lacks the JS runtime yt-dlp needs to resolve
  video formats, but storyboard (image) formats are always available.
  Subtitles download independently of the selected format.
"""

import argparse
import html
import os
import re
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import parse_qs, urlparse


WORKSPACE = os.path.expanduser("~/.openclaw/workspace")
OUTPUT_DIR = os.path.join(WORKSPACE, "youtube-transcripts")
DEFAULT_COOKIES = os.path.join(WORKSPACE, "cookies.txt")
YTDLP_VENV = os.path.join(WORKSPACE, ".venv/bin/yt-dlp")


def find_ytdlp() -> str:
    """Find yt-dlp binary."""
    if os.path.isfile(YTDLP_VENV):
        return YTDLP_VENV
    import shutil
    system = shutil.which("yt-dlp")
    if system:
        return system
    raise RuntimeError(
        f"yt-dlp not found. Expected at {YTDLP_VENV} or in PATH."
    )


def extract_video_id(url: str) -> str:
    """Extract video ID from various YouTube URL formats."""
    parsed = urlparse(url)
    if parsed.hostname in ("youtu.be",):
        return parsed.path.lstrip("/").split("/")[0]
    if parsed.hostname in ("www.youtube.com", "youtube.com", "m.youtube.com"):
        if parsed.path == "/watch":
            return parse_qs(parsed.query)["v"][0]
        if parsed.path.startswith(("/embed/", "/v/", "/shorts/")):
            return parsed.path.split("/")[2]
    if re.fullmatch(r"[\w-]{11}", url):
        return url
    raise ValueError(f"Could not extract video ID from: {url}")


def format_timestamp(seconds: float) -> str:
    """Convert seconds to HH:MM:SS or MM:SS."""
    m, s = divmod(int(seconds), 60)
    h, m = divmod(m, 60)
    return f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"


def parse_vtt_time(time_str: str) -> float:
    """Convert VTT timestamp to seconds."""
    parts = time_str.strip().replace(",", ".").split(":")
    if len(parts) == 3:
        return int(parts[0]) * 3600 + int(parts[1]) * 60 + float(parts[2])
    elif len(parts) == 2:
        return int(parts[0]) * 60 + float(parts[1])
    raise ValueError(f"Invalid VTT time: {time_str}")


def parse_vtt(vtt_text: str) -> list[dict]:
    """Parse YouTube auto-caption VTT into clean transcript segments.

    YouTube auto-captions have a specific structure:
    - "Real" cues (multi-second): 2 text lines. Line 1 repeats previous cue,
      line 2 (with <c> word-timing tags) is new content.
    - "Snapshot" cues (~10ms duration): single plain-text line echoing the
      previous cue's new content. These are purely for display continuity.

    This parser extracts only the genuinely new text from each real cue.
    """
    blocks = re.split(r"\n\s*\n", vtt_text.replace("\r\n", "\n").strip())
    segments = []

    for block in blocks:
        raw_lines = block.split("\n")
        lines = [l for l in raw_lines if l.strip()]
        if not lines or lines[0].strip().upper().startswith("WEBVTT"):
            continue

        # Find the timing line
        timing_idx = None
        for i, line in enumerate(lines[:2]):
            if "-->" in line:
                timing_idx = i
                break
        if timing_idx is None:
            continue

        # Parse start and end times
        timing = lines[timing_idx].strip()
        time_parts = timing.split("-->")
        start_str = time_parts[0].strip().split(" ")[0]
        end_str = time_parts[1].strip().split(" ")[0]
        try:
            start_sec = parse_vtt_time(start_str)
            end_sec = parse_vtt_time(end_str)
        except ValueError:
            continue

        # Skip "snapshot" cues (duration < 0.1s) — they're just echoes
        if end_sec - start_sec < 0.1:
            continue

        # Get text lines after the timing line
        text_lines = lines[timing_idx + 1:]
        if not text_lines:
            continue

        # For YouTube auto-captions: if there are 2+ text lines,
        # the last line (with <c> tags) has the new content.
        # If there's only 1 line, it's the initial or a simple cue.
        has_tagged = any("<c>" in l or "<00:" in l for l in text_lines)
        if has_tagged and len(text_lines) >= 2:
            # Only keep lines with timing tags (new content)
            new_lines = [l for l in text_lines if "<c>" in l or "<00:" in l]
            text = " ".join(new_lines)
        else:
            text = " ".join(text_lines)

        # Strip VTT formatting tags and clean up
        text = re.sub(r"<[^>]+>", "", text)
        text = html.unescape(text).strip()
        text = re.sub(r"\s+", " ", text)
        if not text:
            continue

        segments.append({"start": start_sec, "text": text})

    # Final dedup: skip consecutive identical text
    deduped = []
    for seg in segments:
        if not deduped or seg["text"] != deduped[-1]["text"]:
            deduped.append(seg)
    return deduped


def fetch_metadata(ytdlp: str, video_id: str, cookies: str) -> dict:
    """Get video metadata using --print (lightweight, no format resolution)."""
    url = f"https://www.youtube.com/watch?v={video_id}"
    cmd = [
        ytdlp,
        "--cookies", cookies,
        "--no-playlist",
        "--no-warnings",
        "-f", "sb0",
        "--skip-download",
        "--print", "%(title)s\t%(channel)s\t%(view_count)s\t%(upload_date)s",
        url,
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
    if result.returncode != 0:
        # Non-fatal — we can still get the transcript without metadata
        print(f"Warning: metadata fetch failed: {result.stderr.strip()}", file=sys.stderr)
        return {}

    parts = result.stdout.strip().split("\t")
    if len(parts) >= 4:
        meta = {
            "title": parts[0] if parts[0] != "NA" else None,
            "channel": parts[1] if parts[1] != "NA" else None,
            "view_count": parts[2] if parts[2] != "NA" else None,
            "upload_date": parts[3] if parts[3] != "NA" else None,
        }
        # Convert view_count to int if possible
        try:
            meta["view_count"] = int(meta["view_count"])
        except (ValueError, TypeError):
            pass
        return meta
    return {}


def fetch_transcript(ytdlp: str, video_id: str, lang: str, cookies: str) -> list[dict]:
    """Download auto-subtitles and parse them."""
    url = f"https://www.youtube.com/watch?v={video_id}"

    with tempfile.TemporaryDirectory() as tmpdir:
        outtmpl = os.path.join(tmpdir, "%(id)s.%(ext)s")
        cmd = [
            ytdlp,
            "--cookies", cookies,
            "-f", "sb0",
            "--skip-download",
            "--no-playlist",
            "--write-auto-sub",
            "--sub-lang", lang,
            "--sub-format", "vtt",
            "-o", outtmpl,
            url,
        ]
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)

        if result.returncode != 0:
            stderr = result.stderr.strip()
            if "Sign in to confirm" in stderr or "bot" in stderr.lower():
                raise RuntimeError(
                    "YouTube bot detection triggered. Cookies may have expired.\n"
                    "Re-export cookies from your browser and upload to the VPS:\n"
                    "  On Mac: yt-dlp --cookies-from-browser chrome --cookies /tmp/yt-cookies.txt "
                    "--skip-download --no-playlist 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'\n"
                    f"  Then: scp /tmp/yt-cookies.txt openclaw@100.67.133.40:{DEFAULT_COOKIES}"
                )
            raise RuntimeError(f"yt-dlp failed:\n{stderr}\n{result.stdout.strip()}")

        # Find the VTT file
        vtt_files = sorted(Path(tmpdir).glob("*.vtt"))
        if not vtt_files:
            print("No subtitle file was written. Video may not have captions.", file=sys.stderr)
            return []

        best = []
        for vtt_file in vtt_files:
            segments = parse_vtt(vtt_file.read_text(encoding="utf-8", errors="replace"))
            if len(segments) > len(best):
                best = segments
        return best


def build_markdown(video_id: str, meta: dict, segments: list[dict]) -> str:
    """Build markdown from metadata + transcript segments."""
    title = meta.get("title") or f"YouTube Video {video_id}"
    channel = meta.get("channel") or "Unknown"
    url = f"https://www.youtube.com/watch?v={video_id}"
    date = datetime.now(timezone.utc).strftime("%Y-%m-%d")

    lines = [
        f"# {title}",
        "",
        "| Field | Value |",
        "| --- | --- |",
        f"| **Video ID** | `{video_id}` |",
        f"| **URL** | <{url}> |",
        f"| **Channel** | {channel} |",
        f"| **Date Captured** | {date} |",
    ]
    if meta.get("view_count") is not None:
        try:
            lines.append(f"| **Views** | {int(meta['view_count']):,} |")
        except (ValueError, TypeError):
            lines.append(f"| **Views** | {meta['view_count']} |")
    if meta.get("upload_date"):
        raw = str(meta["upload_date"])
        if len(raw) == 8:
            pub = f"{raw[:4]}-{raw[4:6]}-{raw[6:]}"
        else:
            pub = raw
        lines.append(f"| **Published** | {pub} |")

    lines.extend(["", "---", "", "## Transcript", ""])

    if segments:
        for seg in segments:
            ts = format_timestamp(seg["start"])
            lines.append(f"**[{ts}]** {seg['text']}")
            lines.append("")
    else:
        lines.append("*No transcript available for this video.*")
        lines.append("")

    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(
        description="Grab a YouTube transcript and save as markdown."
    )
    parser.add_argument("url", help="YouTube video URL or video ID")
    parser.add_argument("-l", "--lang", default="en", help="Subtitle language (default: en)")
    parser.add_argument(
        "-c", "--cookies",
        default=os.environ.get("YT_COOKIES", DEFAULT_COOKIES),
        help=f"Path to cookies.txt (default: {DEFAULT_COOKIES})",
    )
    args = parser.parse_args()

    if not os.path.isfile(args.cookies):
        print(
            f"Error: Cookies file not found at {args.cookies}\n"
            "YouTube blocks this server's IP without cookies.\n"
            "Export cookies from a browser and upload them:\n"
            f"  scp cookies.txt openclaw@100.67.133.40:{DEFAULT_COOKIES}",
            file=sys.stderr,
        )
        sys.exit(1)

    video_id = extract_video_id(args.url)
    ytdlp = find_ytdlp()
    print(f"Video ID: {video_id}")

    meta = fetch_metadata(ytdlp, video_id, args.cookies)
    title = meta.get("title", "Unknown")
    print(f"Title: {title}")

    segments = fetch_transcript(ytdlp, video_id, args.lang, args.cookies)
    print(f"Transcript segments: {len(segments)}")

    md = build_markdown(video_id, meta, segments)

    os.makedirs(OUTPUT_DIR, exist_ok=True)
    out_path = os.path.join(OUTPUT_DIR, f"{video_id}.md")
    with open(out_path, "w", encoding="utf-8") as f:
        f.write(md)

    print(f"Saved to {out_path}")


if __name__ == "__main__":
    main()