"""Base class for BADAS video processors — shared FFmpeg/OpenCV I/O logic."""

import cv2
import subprocess
from pathlib import Path
from typing import Dict, Any, List


class BaseVideoProcessor:
    """Shared FFmpeg/OpenCV video I/O logic for overlay and heatmap processors.

    Subclasses (VideoOverlayProcessor, HeatmapVideoProcessor) inherit:
    - _has_ffmpeg flag
    - _open_video()       — open a cv2.VideoCapture with basic validation
    - _open_ffmpeg_pipe() — launch the standard libx264 FFmpeg pipe
    - _create_frame_lookup() — map every frame index to the nearest prediction
    """

    def __init__(self):
        from badas.visualization.renderer import _check_ffmpeg
        self._has_ffmpeg = _check_ffmpeg()

    # ── Video open ────────────────────────────────────────────────────────────

    def _open_video(self, video_path: str):
        """Open video capture and return (cap, fps, width, height, total_frames).

        Raises:
            FileNotFoundError: if the file does not exist.
            RuntimeError: if the video reports 0 frames or 0 fps.
        """
        if not Path(video_path).exists():
            raise FileNotFoundError(f"Video not found: {video_path!r}")
        cap = cv2.VideoCapture(video_path)
        fps = cap.get(cv2.CAP_PROP_FPS)
        width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
        if fps == 0 or total_frames == 0:
            cap.release()
            raise RuntimeError(
                f"Failed to open video (0 frames / 0 fps): {video_path!r}"
            )
        return cap, fps, width, height, total_frames

    # ── FFmpeg ────────────────────────────────────────────────────────────────

    def _open_ffmpeg_pipe(
        self, width: int, height: int, fps: float, output_path: str
    ):
        """Launch an FFmpeg raw-BGR → libx264 encoding pipe.

        Returns a ``subprocess.Popen`` object whose ``stdin`` accepts raw
        BGR24 frames (write ``frame.tobytes()`` per frame).
        """
        cmd = [
            'ffmpeg', '-y',
            '-f', 'rawvideo', '-vcodec', 'rawvideo',
            '-s', f'{width}x{height}', '-pix_fmt', 'bgr24',
            '-r', str(fps), '-i', '-',
            '-c:v', 'libx264', '-preset', 'fast',
            '-pix_fmt', 'yuv420p', '-movflags', '+faststart',
            output_path,
        ]
        return subprocess.Popen(
            cmd,
            stdin=subprocess.PIPE,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.PIPE,
        )

    # ── Frame lookup ──────────────────────────────────────────────────────────

    def _create_frame_lookup(
        self,
        predictions: List[Dict[str, Any]],
        fps: float,
        total_frames: int,
    ) -> Dict[int, Dict[str, Any]]:
        """Map every frame index to the nearest prediction (midpoint split).

        For each pair of consecutive predictions the boundary is placed at
        the midpoint between their centre frames, so each prediction
        "owns" the frames closest to it.
        """
        frame_lookup: Dict[int, Dict[str, Any]] = {}
        if not predictions:
            return frame_lookup
        sorted_preds = sorted(predictions, key=lambda p: p['timestamp'])
        pred_frames = [(int(p['timestamp'] * fps), p) for p in sorted_preds]
        for i, (fn, pred) in enumerate(pred_frames):
            start = fn if i == 0 else (pred_frames[i - 1][0] + fn) // 2
            end = (total_frames if i == len(pred_frames) - 1
                   else (fn + pred_frames[i + 1][0]) // 2)
            for f in range(start, min(end, total_frames)):
                frame_lookup[f] = pred
        return frame_lookup
