#!/usr/bin/env python3
"""
Podcast audio generator.

Usage:
    python3 generate_audio.py <script_file> <episode_json> [episodes_output_dir]

Script format (plain text):
    HOST_A: Welcome to the show...
    HOST_B: Great to be here...
    HOST_A: Today we're talking about...

Episode JSON:
    {
        "title": "Episode title",
        "description": "Short description",
        "topic": "original topic",
        "date": "2026-03-06",
        "voice_map": {"HOST_A": "en-US-EmmaNeural", "HOST_B": "en-GB-RyanNeural"}  // optional
    }

Voices (default):
    HOST_A → en-US-EmmaNeural  (US female)
    HOST_B → en-GB-RyanNeural  (UK male)

Output:
    - Saves MP3 to episodes_output_dir (default: episodes/ next to this script)
    - Saves sidecar .json alongside MP3
    - Calls rebuild_feed.py to regenerate feed.xml

Episode numbers are assigned at feed-rebuild time (positional by date/filename).
No shared counter file — safe for concurrent runs.
"""

import sys
import os
import json
import asyncio
import subprocess
import tempfile
import re
from datetime import datetime
from pathlib import Path

BASE_DIR    = Path(__file__).parent
BASE_URL    = "https://earth.tail8566f7.ts.net"

DEFAULT_VOICES = {
    "HOST_A": "en-US-EmmaNeural",
    "HOST_B": "en-GB-RyanNeural",
}
VOICES = dict(DEFAULT_VOICES)


# ── Script parsing ────────────────────────────────────────────────────────────

def parse_script(text: str) -> list[tuple[str, str]]:
    """Parse HOST_A/HOST_B script into list of (speaker, text) tuples."""
    lines = []
    current_speaker = None
    current_text = []

    for line in text.strip().splitlines():
        line = line.strip()
        if not line:
            continue
        m = re.match(r'^(HOST_[AB]):\s*(.*)', line)
        if m:
            if current_speaker and current_text:
                lines.append((current_speaker, " ".join(current_text)))
            current_speaker = m.group(1)
            current_text = [m.group(2)] if m.group(2) else []
        elif current_speaker:
            current_text.append(line)

    if current_speaker and current_text:
        lines.append((current_speaker, " ".join(current_text)))

    return lines


# ── TTS ───────────────────────────────────────────────────────────────────────

async def generate_segment(speaker: str, text: str, out_path: str, retries: int = 5):
    import edge_tts
    voice = VOICES.get(speaker, VOICES["HOST_A"])
    last_err = None
    for attempt in range(retries):
        try:
            communicate = edge_tts.Communicate(text, voice)
            await communicate.save(out_path)
            return
        except Exception as e:
            last_err = e
            wait = 2 ** attempt + 1
            print(f"    ⚠️  Attempt {attempt+1} failed ({e}), retrying in {wait}s…")
            await asyncio.sleep(wait)
    raise RuntimeError(f"Failed after {retries} attempts: {last_err}")


async def generate_all_segments(lines: list, tmp_dir: str) -> list[str]:
    paths = []
    total = len(lines)
    for i, (speaker, text) in enumerate(lines):
        out = os.path.join(tmp_dir, f"seg_{i:04d}.mp3")
        print(f"  [{i+1}/{total}] {speaker}: {text[:60]}{'...' if len(text)>60 else ''}")
        await generate_segment(speaker, text, out)
        paths.append(out)
        # small pause to avoid rate-limiting
        await asyncio.sleep(0.3)
    return paths


# ── Audio stitching ───────────────────────────────────────────────────────────

def stitch_audio(segment_paths: list[str], out_path: str):
    with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
        concat_list = f.name
        for p in segment_paths:
            f.write(f"file '{p}'\n")

    cmd = [
        "ffmpeg", "-y",
        "-f", "concat", "-safe", "0",
        "-i", concat_list,
        "-c:a", "libmp3lame", "-q:a", "4",
        "-ar", "44100", "-ac", "1",
        out_path
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    os.unlink(concat_list)

    if result.returncode != 0:
        print("ffmpeg error:", result.stderr[-500:])
        raise RuntimeError("ffmpeg failed")


# ── Helpers ───────────────────────────────────────────────────────────────────

def get_audio_duration(mp3_path: str) -> int:
    result = subprocess.run(
        ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", mp3_path],
        capture_output=True, text=True
    )
    if result.returncode == 0:
        try:
            data = json.loads(result.stdout)
            return int(float(data["format"].get("duration", 0)))
        except Exception:
            pass
    return 0


def format_duration(seconds: int) -> str:
    h = seconds // 3600
    m = (seconds % 3600) // 60
    s = seconds % 60
    return f"{h:02d}:{m:02d}:{s:02d}"


# ── Main ──────────────────────────────────────────────────────────────────────

def main():
    if len(sys.argv) < 3:
        print("Usage: generate_audio.py <script_file> <episode_json> [episodes_output_dir]")
        sys.exit(1)

    script_path  = sys.argv[1]
    episode_path = sys.argv[2]

    # Determine output directory (and thus which show's feed to rebuild)
    if len(sys.argv) > 3:
        episodes_dir = Path(sys.argv[3])
    else:
        episodes_dir = BASE_DIR / "episodes"

    episodes_dir.mkdir(parents=True, exist_ok=True)

    # show_dir = parent of episodes/
    show_dir = episodes_dir.parent

    with open(script_path, "r") as f:
        script_text = f.read()

    with open(episode_path, "r") as f:
        episode = json.load(f)

    # Apply per-episode voice map if specified
    if "voice_map" in episode:
        VOICES.update(episode["voice_map"])
        print(f"🎤  Voice map override: {episode['voice_map']}")

    # Build output filename from date + topic slug
    date_slug  = episode["date"].replace("-", "")
    topic_slug = re.sub(r'[^a-z0-9]+', '_', episode["topic"].lower()).strip('_')[:40]
    mp3_filename = f"{date_slug}_{topic_slug}.mp3"
    mp3_path     = str(episodes_dir / mp3_filename)

    print(f"\n🎙️  Generating: {episode['title']}")
    print(f"📄  Script:  {script_path}")
    print(f"🎵  Output:  {mp3_path}\n")

    lines = parse_script(script_text)
    print(f"📝  {len(lines)} dialogue lines parsed\n")

    # Generate + stitch audio
    with tempfile.TemporaryDirectory() as tmp_dir:
        print("🔊  Generating audio segments...")
        segment_paths = asyncio.run(generate_all_segments(lines, tmp_dir))
        print(f"\n✂️   Stitching {len(segment_paths)} segments...")
        stitch_audio(segment_paths, mp3_path)

    duration = get_audio_duration(mp3_path)
    size     = os.path.getsize(mp3_path)
    dur_str  = format_duration(duration)

    print(f"\n✅  Audio: {mp3_path}")
    print(f"⏱️   Duration: {dur_str}  |  Size: {size // 1024} KB")

    # Save sidecar JSON — NO episode_num (assigned at rebuild time)
    sidecar = {
        "title":       episode["title"],
        "description": episode["description"],
        "topic":       episode.get("topic", ""),
        "date":        episode["date"],
        "duration":    dur_str,
        "size_bytes":  size,
    }
    sidecar_path = str(episodes_dir / mp3_filename.replace(".mp3", ".json"))
    with open(sidecar_path, "w") as f:
        json.dump(sidecar, f, indent=2)
    print(f"💾  Sidecar: {sidecar_path}")

    # Rebuild feed from all sidecars for this show
    print(f"\n📡  Rebuilding RSS feed for show: {show_dir} ...")
    result = subprocess.run(
        [sys.executable, str(BASE_DIR / "rebuild_feed.py"), str(show_dir)],
        capture_output=True, text=True
    )
    print(result.stdout)
    if result.returncode != 0:
        print("Feed rebuild error:", result.stderr)

    ep_url   = f"{BASE_URL}{show_dir.relative_to(BASE_DIR.parent.parent.parent.parent) if show_dir != BASE_DIR else '/podcast'}/episodes/{mp3_filename}"
    feed_url = f"{BASE_URL}/podcast/feed.xml" if show_dir == BASE_DIR else f"{BASE_URL}/podcast/{show_dir.name}/feed.xml"

    print(f"\n🔗  Feed URL: {feed_url}")
    print(f"\n🎉  Done!")

    # Return structured result for caller
    print("\n--- RESULT_JSON ---")
    print(json.dumps({
        "mp3_filename": mp3_filename,
        "feed_url":     feed_url,
        "duration":     dur_str,
        "size_kb":      size // 1024,
        "title":        episode["title"],
    }))


if __name__ == "__main__":
    main()
