"""Timeline — the top-level composition object.

Mirrors VideoDB's ``Timeline`` with local rendering via FFmpeg.
"""
from __future__ import annotations

import os
from typing import Callable, Optional, Union

from ._asset import AudioAsset, ImageAsset, TextAsset, VideoAsset
from ._clip import Clip
from ._errors import ComposeError, ValidationError
from ._ffmpeg import MediaInfo, _DownloadCache, probe, run_ffmpeg
from ._filter_graph import (
    FilterGraphResult,
    _ResolvedClip,
    _compute_effective_duration,
    build_filter_graph,
)
from ._resolution import resolve_resolution
from ._track import Track
from ._types import AspectRatio, parse_aspect_ratio


_OUTPUT_CODECS: dict[str, dict[str, str | None]] = {
    "mp4": {"vcodec": "libx264", "acodec": "aac", "pix_fmt": "yuv420p"},
    "mp4h265": {"vcodec": "libx265", "acodec": "aac", "pix_fmt": "yuv420p"},
    "webm": {"vcodec": "libvpx-vp9", "acodec": "libopus", "pix_fmt": "yuv420p"},
    "mov": {"vcodec": "prores_ks", "acodec": "pcm_s16le", "pix_fmt": "yuva444p10le"},
    "gif": {"vcodec": "gif", "acodec": None, "pix_fmt": "rgb8"},
}

_QUALITY_PRESETS: dict[str, dict[str, str | int]] = {
    "low": {"preset": "ultrafast", "crf": 28},
    "medium": {"preset": "medium", "crf": 23},
    "high": {"preset": "slow", "crf": 18},
    "lossless": {"preset": "veryslow", "crf": 0},
}


class Timeline:
    """Top-level composition canvas.

    Usage::

        timeline = Timeline()
        timeline.aspect_ratio = "16:9"
        timeline.background = "#000000"
        timeline.add_track(track)
        timeline.render("output.mp4", resolution="1080p")
    """

    aspect_ratio: str | None
    background: str
    _tracks: list[Track]

    def __init__(
        self,
        *,
        aspect_ratio: Union[str, AspectRatio, None] = None,
        background: str = "#000000",
    ) -> None:
        if isinstance(aspect_ratio, AspectRatio):
            aspect_ratio = aspect_ratio.value
        self.aspect_ratio = aspect_ratio
        self.background = background
        self._tracks = []

    def add_track(self, track: Track) -> None:
        """Append a track.  Add order determines z-order (later = on top)."""
        self._tracks.append(track)

    @property
    def tracks(self) -> list[Track]:
        return list(self._tracks)

    def render(
        self,
        output: str,
        *,
        resolution: str = "1080p",
        format: Optional[str] = None,
        fps: int = 30,
        quality: str = "medium",
        progress: Optional[Callable[[float], None]] = None,
    ) -> str:
        """Render the timeline to a local file.

        Args:
            output: Output file path.
            resolution: Resolution preset (``"1080p"``) or ``"WxH"`` string.
            format: Output format key (``"mp4"``, ``"webm"``, ``"gif"``, etc.).
                Auto-detected from *output* extension if not specified.
            fps: Output frame rate.
            quality: Quality preset (``"low"``/``"medium"``/``"high"``/``"lossless"``).
            progress: Optional callback receiving current rendered time (seconds).

        Returns:
            The *output* path.
        """
        if not self._tracks:
            raise ValidationError("Timeline has no tracks")

        # Resolve output format
        fmt_key = format
        if fmt_key is None:
            ext = os.path.splitext(output)[1].lstrip(".").lower()
            fmt_key = ext if ext in _OUTPUT_CODECS else "mp4"
        if fmt_key not in _OUTPUT_CODECS:
            raise ValidationError(
                f"Unknown format: {fmt_key!r}. Choose from: {', '.join(_OUTPUT_CODECS)}"
            )
        codec_cfg = _OUTPUT_CODECS[fmt_key]

        # Resolve resolution
        w, h = resolve_resolution(resolution, self.aspect_ratio)

        # Quality
        q = _QUALITY_PRESETS.get(quality, _QUALITY_PRESETS["medium"])

        # Resolve all assets: download URLs, probe media
        cache = _DownloadCache()
        try:
            resolved: dict[int, list[_ResolvedClip]] = {}
            for tidx, track in enumerate(self._tracks):
                rclips: list[_ResolvedClip] = []
                for entry in track.entries:
                    clip = entry.clip
                    asset = clip.asset

                    local_path = ""
                    info: MediaInfo | None = None

                    if isinstance(asset, TextAsset):
                        pass  # no file
                    elif hasattr(asset, "_is_url") and asset._is_url():
                        local_path = cache.resolve(asset.source)
                    else:
                        local_path = asset.source

                    if local_path and not isinstance(asset, TextAsset):
                        info = probe(local_path)

                    eff_dur = _compute_effective_duration(clip, info)

                    rclips.append(
                        _ResolvedClip(
                            entry=entry,
                            info=info,
                            local_path=local_path,
                            effective_duration=eff_dur,
                        )
                    )
                resolved[tidx] = rclips

            # Build filter graph
            bg_color = self.background.lstrip("#") if self.background.startswith("#") else self.background
            # FFmpeg color format: 0x prefix
            if len(bg_color) == 6:
                bg_ffmpeg = f"0x{bg_color}"
            else:
                bg_ffmpeg = bg_color

            fg = build_filter_graph(
                self._tracks,
                resolved,
                width=w,
                height=h,
                fps=fps,
                bg_color=bg_ffmpeg,
            )

            # Assemble ffmpeg command
            args: list[str] = list(fg.input_args)
            args += ["-filter_complex", fg.filter_complex]

            # Map outputs
            v_out = fg.video_out_label
            if v_out.startswith("[") and v_out.endswith("]"):
                args += ["-map", v_out]
            else:
                args += ["-map", v_out]

            if fg.audio_out_label and codec_cfg["acodec"] is not None:
                a_out = fg.audio_out_label
                args += ["-map", a_out]

            # Codec settings
            vcodec = codec_cfg["vcodec"]
            args += ["-c:v", str(vcodec)]

            if vcodec in ("libx264", "libx265"):
                args += ["-preset", str(q["preset"]), "-crf", str(q["crf"])]

            pf = codec_cfg.get("pix_fmt")
            if pf and vcodec != "gif":
                args += ["-pix_fmt", str(pf)]

            if codec_cfg["acodec"] is not None and fg.audio_out_label:
                args += ["-c:a", str(codec_cfg["acodec"])]
                if codec_cfg["acodec"] == "aac":
                    args += ["-b:a", "192k"]

            if fmt_key in ("mp4", "mp4h265"):
                args += ["-movflags", "+faststart"]

            if fmt_key == "gif":
                args += ["-r", str(fps)]

            args.append(output)

            # Compute total duration for progress
            total_dur = 0.0
            for rclips in resolved.values():
                for rc in rclips:
                    clip_end = rc.entry.start + rc.effective_duration
                    total_dur = max(total_dur, clip_end)

            run_ffmpeg(args, total_duration=total_dur, progress=progress)

        finally:
            cache.cleanup()

        return output

    def preview_command(
        self,
        output: str = "output.mp4",
        *,
        resolution: str = "1080p",
        fps: int = 30,
    ) -> list[str]:
        """Return the ffmpeg command that ``render()`` would execute (for debugging)."""
        # Minimal implementation: just show the structure
        w, h = resolve_resolution(resolution, self.aspect_ratio)
        return [
            "ffmpeg", "-y",
            f"# {len(self._tracks)} tracks, {w}x{h}@{fps}fps",
            "→", output,
        ]

    def __repr__(self) -> str:
        ar = self.aspect_ratio or "auto"
        return f"Timeline(aspect_ratio={ar!r}, tracks={len(self._tracks)})"
