"""
src/cv/frame_extractor.py — Low-level video frame access

Responsibility:
  Provide a thin, testable wrapper around cv2.VideoCapture for:
    - seeking to an exact timestamp and returning one BGR frame
    - iterating frames with a configurable step size
    - extracting the "representative" middle frame of a Scene / TrailerBeat

No fingerprinting, no matching — only raw frame delivery.
"""

from __future__ import annotations

import logging
from contextlib import contextmanager
from pathlib import Path
from typing import Generator, Iterator

import cv2
import numpy as np

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Context-managed VideoCapture
# ---------------------------------------------------------------------------

@contextmanager
def open_video(path: Path) -> Generator[cv2.VideoCapture, None, None]:
    """
    Context manager that opens a VideoCapture and guarantees release.

    Args:
        path: Absolute path to the video file.

    Raises:
        FileNotFoundError: If the file does not exist.
        RuntimeError:      If OpenCV cannot open the file.
    """
    if not path.exists():
        raise FileNotFoundError(f"Video not found: {path}")

    cap = cv2.VideoCapture(str(path))
    if not cap.isOpened():
        raise RuntimeError(f"OpenCV could not open video: {path}")

    try:
        yield cap
    finally:
        cap.release()


# ---------------------------------------------------------------------------
# Video metadata
# ---------------------------------------------------------------------------

def get_video_info(path: Path) -> dict[str, float | int]:
    """
    Return basic metadata without keeping the file open.

    Returns:
        dict with keys: fps, frame_count, duration_s, width, height
    """
    with open_video(path) as cap:
        fps         = cap.get(cv2.CAP_PROP_FPS)
        frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
        width       = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        height      = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

    duration_s = frame_count / fps if fps > 0 else 0.0
    return {
        "fps":         fps,
        "frame_count": frame_count,
        "duration_s":  duration_s,
        "width":       width,
        "height":      height,
    }


# ---------------------------------------------------------------------------
# Single frame extraction
# ---------------------------------------------------------------------------

def grab_frame_at(cap: cv2.VideoCapture, timestamp_s: float) -> np.ndarray | None:
    """
    Seek to *timestamp_s* and return the BGR frame at that position.

    Uses CAP_PROP_POS_MSEC for sub-frame accuracy.

    Args:
        cap:         An already-open VideoCapture.
        timestamp_s: Target time in seconds.

    Returns:
        BGR ndarray (H, W, 3) or None if seeking / decoding failed.
    """
    cap.set(cv2.CAP_PROP_POS_MSEC, timestamp_s * 1000.0)
    ok, frame = cap.read()
    if not ok or frame is None:
        logger.debug("grab_frame_at: failed at %.3fs", timestamp_s)
        return None
    return frame


def grab_frame_at_path(path: Path, timestamp_s: float) -> np.ndarray | None:
    """
    One-shot convenience: open → seek → grab → release.
    Prefer open_video() when grabbing multiple frames from the same file.
    """
    with open_video(path) as cap:
        return grab_frame_at(cap, timestamp_s)


# ---------------------------------------------------------------------------
# Middle-frame extraction (representative frame for fingerprinting)
# ---------------------------------------------------------------------------

def grab_midpoint_frame(
    cap: cv2.VideoCapture,
    start_s: float,
    end_s: float,
) -> np.ndarray | None:
    """
    Grab the frame at the exact midpoint of a [start_s, end_s] interval.

    Args:
        cap:     Open VideoCapture for the source video.
        start_s: Interval start in seconds.
        end_s:   Interval end in seconds.

    Returns:
        BGR frame or None if decoding failed.
    """
    mid = start_s + (end_s - start_s) / 2.0
    return grab_frame_at(cap, mid)


# ---------------------------------------------------------------------------
# Stepped-frame iterator (used by Deep Scan coarse pass)
# ---------------------------------------------------------------------------

def iter_frames_stepped(
    cap: cv2.VideoCapture,
    start_s: float,
    end_s: float,
    step_s: float,
) -> Iterator[tuple[float, np.ndarray]]:
    """
    Yield (timestamp_s, frame) for every *step_s* increment in [start_s, end_s].

    Frames that fail to decode are silently skipped.

    Args:
        cap:     Open VideoCapture.
        start_s: Scan window start in seconds.
        end_s:   Scan window end   in seconds.
        step_s:  Step between samples in seconds.

    Yields:
        (timestamp_s, bgr_frame)
    """
    if step_s <= 0:
        raise ValueError(f"step_s must be > 0; got {step_s}")

    t = start_s
    while t <= end_s:
        frame = grab_frame_at(cap, t)
        if frame is not None:
            yield t, frame
        t = round(t + step_s, 6)  # avoid float accumulation drift
