"""Video composer — combine recording webm + per-scene audio + caption
overlays into final mp4.

Ported from demos/narrate.py's compose_video(). Uses moviepy + ffmpeg.
moviepy is lazy-imported; this module itself is import-safe without the
[video] extra installed.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from pathlib import Path

log = logging.getLogger(__name__)


@dataclass
class ComposeResult:
    """Output of compose()."""

    path: Path
    duration_seconds: float
    audio_clip_count: int


def compose(
    *,
    video_path: Path,
    audio_clips: list[tuple[str, Path, float]],  # [(scene_id, audio_path, mark_time), ...]
    output_path: Path,
    fps: int = 30,
) -> ComposeResult:
    """Compose final mp4 from a Playwright recording + scene audio clips.

    Each audio clip is placed at its scene's wall-clock mark time. No
    overlap shifting — duration-driven sync (TTS pre-generated, Playwright
    paced to match per-scene durations). Any audio that would extend past
    the video's end is truncated to fit.

    Args:
        video_path: Path to the raw webm recording.
        audio_clips: List of (scene_id, audio_mp3_path, mark_time_seconds).
            mark_time is the wall-clock offset at which the scene began
            during recording — exactly when the audio should start.
        output_path: Destination mp4 path. Parent dirs auto-created.
        fps: Output frame rate (default 30).

    Returns:
        ComposeResult with path + duration + clips-actually-placed count.

    Raises:
        ImportError: if moviepy isn't installed.
        FileNotFoundError: if video_path doesn't exist.
        ValueError: if no audio clips are usable.
    """

    try:
        from moviepy import AudioFileClip, CompositeAudioClip, VideoFileClip
    except ImportError as exc:
        raise ImportError(
            "Video composition requires moviepy. Install with: pip install 'agntspace[video]'"
        ) from exc

    if not video_path.exists():
        raise FileNotFoundError(f"Recording not found: {video_path}")

    output_path.parent.mkdir(parents=True, exist_ok=True)

    video = VideoFileClip(str(video_path))
    placed: list[object] = []       # moviepy clips with start/duration set
    raw_clips: list[object] = []    # retained to close later

    try:
        for scene_id, audio_path, mark_time in audio_clips:
            if not audio_path or not audio_path.exists():
                log.warning("compose: missing audio %s for %s; skipping", audio_path, scene_id)
                continue
            clip = AudioFileClip(str(audio_path))
            raw_clips.append(clip)

            if mark_time + clip.duration > video.duration:
                # Truncate if audio would extend past video end
                remaining = max(0.0, video.duration - mark_time - 0.3)
                if remaining <= 1.0:
                    log.warning("compose: %s has insufficient time left in video; skipping", scene_id)
                    continue
                clip = clip.with_duration(remaining)

            clip = clip.with_start(mark_time)
            placed.append(clip)
            log.debug("compose: %s placed at %.1fs (%.1fs)", scene_id, mark_time, clip.duration)

        if not placed:
            raise ValueError("No audio clips placed; composition would be silent")

        composite_audio = CompositeAudioClip(placed)
        final = video.with_audio(composite_audio)

        final.write_videofile(
            str(output_path),
            codec="libx264",
            audio_codec="aac",
            fps=fps,
            preset="medium",
            bitrate="4000k",
            logger=None,    # suppress moviepy's own tqdm bar in library use
        )

        duration = float(video.duration)
        return ComposeResult(
            path=output_path,
            duration_seconds=duration,
            audio_clip_count=len(placed),
        )
    finally:
        try:
            video.close()
        except Exception:  # noqa: BLE001
            pass
        for clip in raw_clips:
            try:
                clip.close()
            except Exception:  # noqa: BLE001
                pass
