"""MoviePy v2.2.1 episode builder.

Composes a full episode video from:
- Intro card (Timmy branding still image + title text)
- Highlight clips with crossfade transitions
- TTS narration audio mixed over video
- Background music from pre-generated library
- Outro card with links / subscribe prompt

MoviePy is an optional dependency.  If it is not installed, all functions
return failure results instead of crashing.

Usage
-----
    from content.composition.episode import build_episode

    result = await build_episode(
        clip_paths=["/tmp/clips/h1.mp4", "/tmp/clips/h2.mp4"],
        narration_path="/tmp/narration.wav",
        output_path="/tmp/episodes/ep001.mp4",
        title="Top Highlights — March 2026",
    )
"""

from __future__ import annotations

import asyncio
import logging
from dataclasses import dataclass, field
from pathlib import Path

from config import settings

logger = logging.getLogger(__name__)


@dataclass
class EpisodeResult:
    """Result of an episode composition attempt."""

    success: bool
    output_path: str | None = None
    duration: float = 0.0
    error: str | None = None
    clip_count: int = 0


@dataclass
class EpisodeSpec:
    """Full specification for a composed episode."""

    title: str
    clip_paths: list[str] = field(default_factory=list)
    narration_path: str | None = None
    music_path: str | None = None
    intro_image: str | None = None
    outro_image: str | None = None
    output_path: str | None = None
    transition_duration: float | None = None

    @property
    def resolved_transition(self) -> float:
        return (
            self.transition_duration
            if self.transition_duration is not None
            else settings.video_transition_duration
        )

    @property
    def resolved_output(self) -> str:
        return self.output_path or str(
            Path(settings.content_episodes_dir) / f"{_slugify(self.title)}.mp4"
        )


def _slugify(text: str) -> str:
    """Convert title to a filesystem-safe slug."""
    import re

    slug = text.lower()
    slug = re.sub(r"[^\w\s-]", "", slug)
    slug = re.sub(r"[\s_]+", "-", slug)
    slug = slug.strip("-")
    return slug[:80] or "episode"


def _moviepy_available() -> bool:
    """Return True if moviepy is importable."""
    try:
        import importlib.util

        return importlib.util.find_spec("moviepy") is not None
    except Exception:
        return False


def _compose_sync(spec: EpisodeSpec) -> EpisodeResult:
    """Synchronous MoviePy composition — run in a thread via asyncio.to_thread."""
    try:
        from moviepy import (  # type: ignore[import]
            AudioFileClip,
            ColorClip,
            CompositeAudioClip,
            ImageClip,
            TextClip,
            VideoFileClip,
            concatenate_videoclips,
        )
    except ImportError as exc:
        return EpisodeResult(success=False, error=f"moviepy not available: {exc}")

    clips = []

    # ── Intro card ────────────────────────────────────────────────────────────
    intro_duration = 3.0
    if spec.intro_image and Path(spec.intro_image).exists():
        intro = ImageClip(spec.intro_image).with_duration(intro_duration)
    else:
        intro = ColorClip(size=(1280, 720), color=(10, 10, 30), duration=intro_duration)
    try:
        title_txt = TextClip(
            text=spec.title,
            font_size=48,
            color="white",
            size=(1200, None),
            method="caption",
        ).with_duration(intro_duration)
        title_txt = title_txt.with_position("center")
        from moviepy import CompositeVideoClip  # type: ignore[import]

        intro = CompositeVideoClip([intro, title_txt])
    except Exception as exc:
        logger.warning("Could not add title text to intro: %s", exc)

    clips.append(intro)

    # ── Highlight clips with crossfade ────────────────────────────────────────
    valid_clips: list = []
    for path in spec.clip_paths:
        if not Path(path).exists():
            logger.warning("Clip not found, skipping: %s", path)
            continue
        try:
            vc = VideoFileClip(path)
            valid_clips.append(vc)
        except Exception as exc:
            logger.warning("Could not load clip %s: %s", path, exc)

    if valid_clips:
        transition = spec.resolved_transition
        for vc in valid_clips:
            try:
                vc = vc.with_effects([])  # ensure no stale effects
                clips.append(vc.crossfadein(transition))
            except Exception:
                clips.append(vc)

    # ── Outro card ────────────────────────────────────────────────────────────
    outro_duration = 5.0
    if spec.outro_image and Path(spec.outro_image).exists():
        outro = ImageClip(spec.outro_image).with_duration(outro_duration)
    else:
        outro = ColorClip(size=(1280, 720), color=(10, 10, 30), duration=outro_duration)
    clips.append(outro)

    if not clips:
        return EpisodeResult(success=False, error="no clips to compose")

    # ── Concatenate ───────────────────────────────────────────────────────────
    try:
        final = concatenate_videoclips(clips, method="compose")
    except Exception as exc:
        return EpisodeResult(success=False, error=f"concatenation failed: {exc}")

    # ── Narration audio ───────────────────────────────────────────────────────
    audio_tracks = []
    if spec.narration_path and Path(spec.narration_path).exists():
        try:
            narr = AudioFileClip(spec.narration_path)
            if narr.duration > final.duration:
                narr = narr.subclipped(0, final.duration)
            audio_tracks.append(narr)
        except Exception as exc:
            logger.warning("Could not load narration audio: %s", exc)

    if spec.music_path and Path(spec.music_path).exists():
        try:
            music = AudioFileClip(spec.music_path).with_volume_scaled(0.15)
            if music.duration < final.duration:
                # Loop music to fill episode duration
                loops = int(final.duration / music.duration) + 1
                from moviepy import concatenate_audioclips  # type: ignore[import]

                music = concatenate_audioclips([music] * loops).subclipped(0, final.duration)
            else:
                music = music.subclipped(0, final.duration)
            audio_tracks.append(music)
        except Exception as exc:
            logger.warning("Could not load background music: %s", exc)

    if audio_tracks:
        try:
            mixed = CompositeAudioClip(audio_tracks)
            final = final.with_audio(mixed)
        except Exception as exc:
            logger.warning("Audio mixing failed, continuing without audio: %s", exc)

    # ── Write output ──────────────────────────────────────────────────────────
    output_path = spec.resolved_output
    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    try:
        final.write_videofile(
            output_path,
            codec=settings.default_video_codec,
            audio_codec="aac",
            logger=None,
        )
    except Exception as exc:
        return EpisodeResult(success=False, error=f"write_videofile failed: {exc}")

    return EpisodeResult(
        success=True,
        output_path=output_path,
        duration=final.duration,
        clip_count=len(valid_clips),
    )


async def build_episode(
    clip_paths: list[str],
    title: str,
    narration_path: str | None = None,
    music_path: str | None = None,
    intro_image: str | None = None,
    outro_image: str | None = None,
    output_path: str | None = None,
    transition_duration: float | None = None,
) -> EpisodeResult:
    """Compose a full episode video asynchronously.

    Wraps the synchronous MoviePy work in ``asyncio.to_thread`` so the
    FastAPI event loop is never blocked.

    Returns
    -------
    EpisodeResult
        Always returns a result; never raises.
    """
    if not _moviepy_available():
        logger.warning("moviepy not installed — episode composition disabled")
        return EpisodeResult(
            success=False,
            error="moviepy not available — install moviepy>=2.0",
        )

    spec = EpisodeSpec(
        title=title,
        clip_paths=clip_paths,
        narration_path=narration_path,
        music_path=music_path,
        intro_image=intro_image,
        outro_image=outro_image,
        output_path=output_path,
        transition_duration=transition_duration,
    )

    try:
        return await asyncio.to_thread(_compose_sync, spec)
    except Exception as exc:
        logger.warning("Episode composition error: %s", exc)
        return EpisodeResult(success=False, error=str(exc))
