#!/usr/bin/env python3
"""
Headless YouTube Shorts Pipeline — vollautomatisch
====================================================
Reddit-Story → LLM (deutsche Kurzgeschichte, ~50s) → edge-tts Audio → Pexels B-Roll → ffmpeg Short

Usage:
    python3 pipeline.py                   # Alles automatisch
    python3 pipeline.py --topic beichten  # r/Beichten
    python3 pipeline.py --steps story     # Nur Story generieren
    python3 pipeline.py --steps render    # Nur Video rendern

Config via env vars:
    OLLAMA_API_KEY  — Ollama Cloud API key
    OLLAMA_BASE_URL — default: https://ollama.com/v1
    STORY_MODEL     — default: deepseek-v4-pro
"""

import argparse, json, os, re, subprocess, sys, time
from datetime import datetime
from pathlib import Path

# ── Config ──────────────────────────────────────────────
PROJECT_DIR = Path(os.environ.get("YT_PROJECT_DIR", "/DATA/AppData/hermes/Projekte/youtube-kanal"))
OUTPUT_DIR  = PROJECT_DIR / "output"
VIDEO_DIR   = PROJECT_DIR / "videos"
AUDIO_DIR   = PROJECT_DIR / "audio"

FFMPEG  = "/DATA/.local/bin/ffmpeg-static"
FFPROBE = "/usr/bin/ffprobe"
TTS     = "/DATA/.local/bin/edge-tts"

OLLAMA_KEY  = os.environ.get("OLLAMA_API_KEY", "eaf9ec548a9f4f789cc2eeb0d7cc61e3.VI7OQTAxEEGB1fqyil_1oJob")
OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "https://ollama.com/v1")
STORY_MODEL = os.environ.get("STORY_MODEL", "deepseek-v4-pro")

PEXELS_AUTH = "Authorization: test"
VOICE       = "de-DE-ConradNeural"
VIDEO_SIZE  = (1080, 1920)  # 9:16 vertical for Shorts
FPS         = 30
MAX_DURATION = 55  # seconds — target ~50s + buffer

BROLL_QUERIES = [
    "nature", "landscape", "city", "timelapse", "ocean", "waves",
    "forest", "aerial", "mountains", "sunset", "rain", "night",
    "clouds", "river", "autumn", "travel", "sky", "water",
]

REDDIT_SOURCES = {
    "confessions": "https://www.reddit.com/r/confessions/top.json?limit=5&t=week",
    "beichten":    "https://www.reddit.com/r/Beichten/top.json?limit=5&t=week",
    "stories":     "https://www.reddit.com/r/stories/top.json?limit=5&t=week",
    "tifu":        "https://www.reddit.com/r/tifu/top.json?limit=5&t=week",
}


def run(cmd, timeout=120):
    p = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
    return p.stdout.strip(), p.stderr.strip(), p.returncode


def get_reddit(topic="confessions"):
    """Fetch a random top Reddit post."""
    print("[1/5] 📡 Reddit-Story holen...")
    url = REDDIT_SOURCES[topic]
    stdout, _, _ = run(f'curl -s -H "User-Agent: HermesYT/1.0" "{url}"')
    data = json.loads(stdout)
    posts = data["data"]["children"]
    best = max(posts, key=lambda p: len(p["data"].get("selftext", "")))
    post = best["data"]
    print(f"   {post['title'][:80]}")
    return post["title"], post["selftext"]


def create_story(title, raw_text):
    """Use Ollama Cloud LLM to craft a punchy German Shorts story (~50s spoken)."""
    print("[2/5] 🧠 Story generieren (LLM)...")
    prompt = f"""Du bist ein professioneller Storyteller für YouTube Shorts.

Hier ist ein Reddit-Post (englisch):
Titel: {title}
Text: {raw_text[:2000]}

AUFGABE: Erzähle eine packende, eigene Kurzgeschichte auf DEUTSCH, inspiriert von diesem Reddit-Post. NICHT übersetzen — eine EIGENE Geschichte daraus bauen.

REGELN:
- Maximal 120 Wörter (das sind ~50 Sekunden gesprochen)
- Spannender Einstieg in den ersten 5 Wörtern
- Klarer Spannungsbogen: Situation → Konflikt → Pointe/Twist
- Umgangssprachlich, emotional, authentisch
- NUR die Geschichte — kein Intro, kein Outro, kein "Vergesst nicht zu liken"
- Kein Hashtag, kein Emoji, keine Meta-Kommentare

Gib NUR den Story-Text zurück. Nichts anderes."""

    body = {
        "model": STORY_MODEL,
        "messages": [{"role": "user", "content": prompt}],
        "temperature": 0.9,
        "max_tokens": 1024,  # Reasoning model needs lots for thinking + output
    }

    stdout, _, rc = run(
        f'curl -s -X POST "{OLLAMA_BASE}/chat/completions" '
        f'-H "Authorization: Bearer {OLLAMA_KEY}" '
        f'-H "Content-Type: application/json" '
        f"-d '{json.dumps(body)}'",
        timeout=60,
    )

    try:
        resp = json.loads(stdout)
        msg = resp["choices"][0]["message"]
        story = msg.get("content", "") or msg.get("reasoning", "")
        story = story.strip()
    except (KeyError, IndexError, json.JSONDecodeError) as e:
        print(f"   ❌ LLM error: {e}\n   Response: {stdout[:300]}")
        sys.exit(1)

    if not story:
        print("   ❌ LLM returned empty story!")
        print(f"   Full response: {stdout[:500]}")
        sys.exit(1)

    story = story.strip('"\'')
    story = re.sub(r'\n{3,}', '\n\n', story)
    words = len(story.split())
    print(f"   {words} Wörter — ziel: ~50s")
    return story


def make_audio(text, out_path):
    print("[3/5] 🎙️  Audio (Conrad)...")
    text_clean = text.replace('"', '\\"').replace('!', '!').replace('?', '?')
    _, stderr, rc = run(
        f'{TTS} --voice {VOICE} --text "{text_clean}" --write-media "{out_path}"',
        timeout=60,
    )
    stdout, _, _ = run(
        f'{FFPROBE} -v error -show_entries format=duration '
        f'-of default=noprint_wrappers=1:nokey=1 "{out_path}"'
    )
    dur = float(stdout.strip())
    print(f"   {dur:.0f}s" + (" ⚠️ zu lang!" if dur > MAX_DURATION else " ✅"))
    return dur


def download_broll():
    print("[4/5] 🎬 B-Roll downloaden...")
    bd = VIDEO_DIR / "broll"
    bd.mkdir(parents=True, exist_ok=True)
    for f in bd.glob("*.mp4"):
        f.unlink()

    downloaded = []
    for query in BROLL_QUERIES:
        stdout, _, _ = run(
            f'curl -s "https://api.pexels.com/videos/search?query={query}&per_page=10" '
            f'-H "{PEXELS_AUTH}"'
        )
        try:
            data = json.loads(stdout)
        except:
            continue
        for i, vid in enumerate(data.get("videos", [])):
            files = vid.get("video_files", [])
            best = None
            best_score = float("inf")
            for f in files:
                w, h = f.get("width", 0), f.get("height", 0)
                if w < 480 or h < 480:
                    continue
                score = abs(w - 1080) + abs(h - 1920)
                if h > w:
                    score -= 500  # bonus for vertical
                if score < best_score:
                    best_score = score
                    best = (w, h, f["link"])
            if best:
                fp = bd / f"broll_{query}_{i:03d}.mp4"
                _, _, rc = run(f'curl -sL -o "{fp}" "{best[2]}"', timeout=60)
                if rc == 0 and fp.stat().st_size > 1000:
                    downloaded.append(str(fp))
        time.sleep(0.3)
    print(f"   {len(downloaded)} clips")
    return downloaded


def normalize_clips(paths):
    nd = VIDEO_DIR / "normalized"
    nd.mkdir(parents=True, exist_ok=True)
    for f in nd.glob("*.mp4"):
        f.unlink()
    result = []
    for i, p in enumerate(paths):
        out = nd / f"c{i:04d}.mp4"
        _, stderr, rc = run(
            f'{FFMPEG} -y -i "{p}" '
            f'-vf "scale={VIDEO_SIZE[0]}:{VIDEO_SIZE[1]}:force_original_aspect_ratio=decrease,'
            f'pad={VIDEO_SIZE[0]}:{VIDEO_SIZE[1]}:(ow-iw)/2:(oh-ih)/2:black,'
            f'fps={FPS},format=yuv420p" '
            f'-c:v libx264 -preset ultrafast -crf 23 '
            f'-g {FPS*2} -keyint_min {FPS} -sc_threshold 0 '
            f'-an "{out}"',
            timeout=60,
        )
        if rc == 0 and out.stat().st_size > 1000:
            result.append(str(out))
    return result


def render_broll(norm_paths, target_dur, out_path):
    print(f"[5/5] 🎞️  Rendern ({len(norm_paths)} clips, {target_dur:.0f}s)...")

    if not norm_paths:
        print("   ⚠️  Fallback: Schwarzvideo")
        run(
            f'{FFMPEG} -y -f lavfi -i '
            f'"color=black:s={VIDEO_SIZE[0]}x{VIDEO_SIZE[1]}:d={target_dur}:r={FPS}" '
            f'-c:v libx264 -preset fast -crf 23 -an "{out_path}"'
        )
        return

    total = 0
    for p in norm_paths:
        out, _, _ = run(
            f'{FFPROBE} -v error -show_entries format=duration '
            f'-of default=noprint_wrappers=1:nokey=1 "{p}"'
        )
        try:
            total += float(out.strip())
        except:
            total += 15

    loops = max(1, int(target_dur / total) + 2)

    cf = VIDEO_DIR / "clist.txt"
    lines = []
    for _ in range(loops):
        for p in norm_paths:
            lines.append(f"file '{p}'")
    cf.write_text("\n".join(lines))

    _, stderr, rc = run(
        f'{FFMPEG} -y -f concat -safe 0 -i "{cf}" '
        f'-t {target_dur} '
        f'-c:v libx264 -preset ultrafast -crf 23 -an "{out_path}"',
        timeout=120,
    )

    if rc != 0:
        print(f"   ❌ Render failed: {stderr[:300]}")
        raise RuntimeError("B-roll render failed")

    mb = Path(out_path).stat().st_size // 1024 // 1024
    print(f"   {mb} MB")


def merge(broll_path, audio_path, out_path):
    print("🎬 Audio + Video mergen...")
    _, _, _ = run(
        f'{FFMPEG} -y -i "{broll_path}" -i "{audio_path}" '
        f'-c:v copy -c:a aac -b:a 128k -shortest -map 0:v:0 -map 1:a:0 "{out_path}"'
    )
    print(f"   ✅ {Path(out_path).stat().st_size // 1024 // 1024} MB")


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--topic", default="confessions", choices=list(REDDIT_SOURCES))
    p.add_argument("--steps", default="all")
    args = p.parse_args()

    steps = set(args.steps.split(",")) if args.steps != "all" else None
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")

    for d in [OUTPUT_DIR, VIDEO_DIR, AUDIO_DIR]:
        d.mkdir(parents=True, exist_ok=True)

    audio_p  = AUDIO_DIR / f"{ts}.mp3"
    broll_p  = VIDEO_DIR / f"{ts}_broll.mp4"
    final_p  = OUTPUT_DIR / f"{ts}.mp4"

    if steps is None or "story" in steps:
        title, raw = get_reddit(args.topic)

    if steps is None or "story" in steps:
        story = create_story(title, raw)
        (AUDIO_DIR / f"{ts}.txt").write_text(story, encoding="utf-8")
    else:
        files = sorted(AUDIO_DIR.glob("*.txt"))
        if not files:
            print("❌ Kein Story-Text gefunden!"); sys.exit(1)
        story = files[-1].read_text(encoding="utf-8")

    if steps is None or "audio" in steps:
        dur = make_audio(story, audio_p)
    else:
        files = sorted(AUDIO_DIR.glob("*.mp3"))
        if not files:
            print("❌ Kein Audio gefunden!"); sys.exit(1)
        audio_p = files[-1]
        out, _, _ = run(
            f'{FFPROBE} -v error -show_entries format=duration '
            f'-of default=noprint_wrappers=1:nokey=1 "{audio_p}"'
        )
        dur = float(out.strip())

    target = min(dur + 3, MAX_DURATION + 5)
    print(f"   ➤ Video: {target:.0f}s")

    if steps is None or "broll" in steps:
        clips = download_broll()
    else:
        clips = [str(p) for p in (VIDEO_DIR / "broll").glob("*.mp4")]

    if steps is None or "render" in steps:
        norm = normalize_clips(clips)
        render_broll(norm, target, broll_p)

    if steps is None or "merge" in steps:
        merge(broll_p, audio_p, final_p)

    print(f"\n✅ Fertig: {final_p}")
    print(f"   Story ({len(story.split())} Wörter):")
    print(f"   {story[:120]}...")


if __name__ == "__main__":
    main()
