"""Shared debug overlay utilities for video rendering."""

import os
import shutil
import subprocess
import time
from pathlib import Path
from typing import Self

import cv2
import numpy as np

from .timing import NULL_TIMER, Timer


def create_video_writer(
    output_path: str,
    width: int,
    height: int,
    display_width: int,
    display_height: int,
    fps: float,
) -> tuple[cv2.VideoWriter, bool, str]:
    """
    Create a video writer with fallback codec support.

    Args:
        output_path: Path for output video
        width: Encoded frame width (from source video)
        height: Encoded frame height (from source video)
        display_width: Display width (considering SAR)
        display_height: Display height (considering SAR)
        fps: Frames per second

    Returns:
        Tuple of (video_writer, needs_resize, used_codec)
    """
    needs_resize = (display_width != width) or (display_height != height)

    # Try browser-compatible codecs first
    # avc1/h264: H.264 (Most compatible)
    # vp09: VP9 (Good compatibility)
    # mp4v: MPEG-4 (Poor browser support, last resort)
    codecs_to_try = ["avc1", "h264", "vp09", "mp4v"]

    writer = None
    used_codec = "mp4v"  # Default fallback

    for codec in codecs_to_try:
        try:
            fourcc = cv2.VideoWriter_fourcc(*codec)
            writer = cv2.VideoWriter(output_path, fourcc, fps, (display_width, display_height))
            if writer.isOpened():
                used_codec = codec
                if codec == "mp4v":
                    print(f"Warning: Fallback to {codec} codec. Video may not play in browsers.")
                break
        except Exception:
            continue

    if writer is None or not writer.isOpened():
        raise ValueError(
            f"Failed to create video writer for {output_path} with dimensions "
            f"{display_width}x{display_height}"
        )

    return writer, needs_resize, used_codec


def write_overlay_frame(
    writer: cv2.VideoWriter, frame: np.ndarray, width: int, height: int
) -> None:
    """
    Write a frame to the video writer with dimension validation.

    Args:
        writer: Video writer instance
        frame: Frame to write
        width: Expected frame width
        height: Expected frame height

    Raises:
        ValueError: If frame dimensions don't match expected dimensions
    """
    # Validate dimensions before writing
    if frame.shape[0] != height or frame.shape[1] != width:
        raise ValueError(
            f"Frame dimensions {frame.shape[1]}x{frame.shape[0]} do not match "
            f"expected dimensions {width}x{height}"
        )
    writer.write(frame)


class BaseDebugOverlayRenderer:
    """Base class for debug overlay renderers with common functionality."""

    def __init__(
        self,
        output_path: str,
        width: int,
        height: int,
        display_width: int,
        display_height: int,
        fps: float,
        timer: Timer | None = None,
    ):
        """
        Initialize overlay renderer.

        Args:
            output_path: Path for output video
            width: Encoded frame width (from source video)
            height: Encoded frame height (from source video)
            display_width: Display width (considering SAR)
            display_height: Display height (considering SAR)
            fps: Frames per second
            timer: Optional Timer for measuring operations
        """
        self.output_path = output_path
        self.width = width
        self.height = height
        self.timer = timer or NULL_TIMER

        # Optimize debug video resolution: Cap max dimension to 720p
        # Reduces software encoding time on single-core Cloud Run instances.
        # while keeping sufficient quality for visual debugging.
        max_dimension = 720
        if max(display_width, display_height) > max_dimension:
            scale = max_dimension / max(display_width, display_height)
            # Ensure dimensions are even for codec compatibility
            self.display_width = int(display_width * scale) // 2 * 2
            self.display_height = int(display_height * scale) // 2 * 2
        else:
            self.display_width = display_width
            self.display_height = display_height

        # Duration of ffmpeg re-encoding (0.0 if not needed)
        self.reencode_duration_s = 0.0
        self.writer, self.needs_resize, self.used_codec = create_video_writer(
            output_path, width, height, self.display_width, self.display_height, fps
        )

    def write_frame(self, frame: np.ndarray) -> None:
        """
        Write frame to output video.

        Args:
            frame: Video frame with shape (height, width, 3)

        Raises:
            ValueError: If frame dimensions don't match expected encoded dimensions
        """
        # Validate frame dimensions match expected encoded dimensions
        frame_height, frame_width = frame.shape[:2]
        if frame_height != self.height or frame_width != self.width:
            raise ValueError(
                f"Frame dimensions ({frame_width}x{frame_height}) don't match "
                f"source dimensions ({self.width}x{self.height}). "
                f"Aspect ratio must be preserved from source video."
            )

        # Resize to display dimensions if needed (to handle SAR)
        if self.needs_resize:
            with self.timer.measure("debug_video_resize"):
                frame = cv2.resize(
                    frame,
                    (self.display_width, self.display_height),
                    interpolation=cv2.INTER_LINEAR,
                )

        with self.timer.measure("debug_video_write"):
            write_overlay_frame(self.writer, frame, self.display_width, self.display_height)

    def close(self) -> None:
        """Release video writer and re-encode if possible."""
        self.writer.release()

        # Post-process with ffmpeg ONLY if we fell back to the incompatible mp4v codec
        if self.used_codec == "mp4v" and shutil.which("ffmpeg"):
            temp_path = None
            try:
                temp_path = str(
                    Path(self.output_path).with_suffix(".temp" + Path(self.output_path).suffix)
                )

                # Convert to H.264 with yuv420p pixel format for browser compatibility
                # -y: Overwrite output file
                # -vcodec libx264: Use H.264 codec
                # -pix_fmt yuv420p: Required for wide browser support (Chrome,
                #                   Safari, Firefox)
                # -preset fast: Reasonable speed/compression tradeoff
                # -crf 23: Standard quality
                # -an: Remove audio (debug video has no audio)
                cmd = [
                    "ffmpeg",
                    "-y",
                    "-i",
                    self.output_path,
                    "-vcodec",
                    "libx264",
                    "-pix_fmt",
                    "yuv420p",
                    "-preset",
                    "fast",
                    "-crf",
                    "23",
                    "-an",
                    temp_path,
                ]

                # Suppress output unless error
                reencode_start = time.time()
                subprocess.run(
                    cmd,
                    check=True,
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.PIPE,
                )
                self.reencode_duration_s = time.time() - reencode_start
                print(f"Debug video re-encoded in {self.reencode_duration_s:.2f}s")

                # Overwrite original file
                os.replace(temp_path, self.output_path)

            except subprocess.CalledProcessError as e:
                print(f"Warning: Failed to re-encode debug video with ffmpeg: {e}")
                if temp_path and os.path.exists(temp_path):
                    os.remove(temp_path)
            except Exception as e:
                print(f"Warning: Error during video post-processing: {e}")
                if temp_path and os.path.exists(temp_path):
                    os.remove(temp_path)

    def __enter__(self) -> Self:
        return self

    def __exit__(self, _exc_type, _exc_val, _exc_tb) -> None:  # type: ignore[no-untyped-def]
        self.close()
