"""
src/cv/deep_scan.py — Phase 2: Frame-accurate template matching (Deep Scan)

Responsibility:
  Given a TrailerBeat and a ranked list of VibeHit candidates, open the
  source video and scan each candidate scene in two passes:

    1. Coarse pass:  step through at coarse_step_seconds intervals,
                     comparing via cv2.matchTemplate.
    2. Refine pass:  if coarse score > threshold, zoom in ± refine_window_seconds
                     at refine_step_seconds resolution to pin the exact in-point.

Returns a MatchResult if a confident hit is found, otherwise None.
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Sequence

import cv2
import numpy as np

from src.core.config import AppConfig
from src.core.models import MatchResult, Scene, TrailerBeat, VibeHit
from src.cv.fingerprinting import text_safe_crop
from src.cv.frame_extractor import (
    grab_frame_at,
    grab_frame_at_path,
    iter_frames_stepped,
    open_video,
)

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Template preparation
# ---------------------------------------------------------------------------

def _prepare_template(
    trailer_beat: TrailerBeat,
    cfg: AppConfig,
    proxy_w: int,
    proxy_h: int,
) -> np.ndarray | None:
    """
    Extract, crop, and resize the representative frame from the trailer beat.

    This frame becomes the cv2.matchTemplate "needle".
    """
    vc = cfg.cv.vibe_check
    ds = cfg.cv.deep_scan

    beat_frame = grab_frame_at_path(
        trailer_beat.trailer_path,
        trailer_beat.midpoint_s,
    )
    if beat_frame is None:
        logger.warning("Beat %d: cannot decode midpoint frame.", trailer_beat.beat_id)
        return None

    cropped = text_safe_crop(beat_frame, vc.crop_top_fraction, vc.crop_bottom_fraction)
    resized = cv2.resize(cropped, (proxy_w, proxy_h), interpolation=cv2.INTER_AREA)
    
    # Crop the template by 10% on all sides to allow sliding window (translation invariance)
    # when matching against the source movie, which might have slight pan/scan shifts.
    margin_y = int(proxy_h * 0.10)
    margin_x = int(proxy_w * 0.10)
    template = resized[margin_y : proxy_h - margin_y, margin_x : proxy_w - margin_x]
    
    return template


# ---------------------------------------------------------------------------
# Single-frame match
# ---------------------------------------------------------------------------

def _match_frame(
    source_frame: np.ndarray,
    template: np.ndarray,
    method: int,
    proxy_w: int,
    proxy_h: int,
    crop_top: float,
    crop_bottom: float,
) -> tuple[float, tuple[int, int]]:
    """
    Run cv2.matchTemplate between *source_frame* and *template*.

    Returns:
        (score, (x, y)) where score ∈ [0, 1] for CCOEFF_NORMED.
    """
    cropped = text_safe_crop(source_frame, crop_top, crop_bottom)
    haystack = cv2.resize(cropped, (proxy_w, proxy_h), interpolation=cv2.INTER_AREA)

    # Match the slightly smaller template inside the full proxy frame
    result = cv2.matchTemplate(haystack, template, method)
    _, max_val, _, max_loc = cv2.minMaxLoc(result)
    return float(max_val), (int(max_loc[0]), int(max_loc[1]))


# ---------------------------------------------------------------------------
# Deep Scan core
# ---------------------------------------------------------------------------

def scan_scene(
    beat: TrailerBeat,
    scene: Scene,
    template: np.ndarray,
    cfg: AppConfig,
) -> tuple[float, float, tuple[int, int]] | None:
    """
    Scan one source scene in two passes (coarse → refine).

    Returns:
        (best_timestamp_s, best_score, best_location) or None if no hit.
    """
    ds = cfg.cv.deep_scan
    vc = cfg.cv.vibe_check
    proxy_w = cfg.video.proxy_width
    proxy_h = cfg.video.proxy_height

    best_t     = scene.start_s
    best_score = 0.0
    best_loc   = (0, 0)

    # ---- Coarse pass --------------------------------------------------------
    with open_video(scene.source_path) as cap:
        for t, frame in iter_frames_stepped(
            cap, scene.start_s, scene.end_s, ds.coarse_step_seconds
        ):
            score, loc = _match_frame(
                frame, template, ds.match_method,
                proxy_w, proxy_h, vc.crop_top_fraction, vc.crop_bottom_fraction,
            )
            if score > best_score:
                best_score = score
                best_t     = t
                best_loc   = loc

        if best_score < ds.match_threshold:
            return None  # scene doesn't contain a match worth refining

        # ---- Refine pass ----------------------------------------------------
        refine_start = max(scene.start_s, best_t - ds.refine_window_seconds)
        refine_end   = min(scene.end_s,   best_t + ds.refine_window_seconds)

        refined_t     = best_t
        refined_score = best_score
        refined_loc   = best_loc

        for t, frame in iter_frames_stepped(
            cap, refine_start, refine_end, ds.refine_step_seconds
        ):
            score, loc = _match_frame(
                frame, template, ds.match_method,
                proxy_w, proxy_h, vc.crop_top_fraction, vc.crop_bottom_fraction,
            )
            if score > refined_score:
                refined_score = score
                refined_t     = t
                refined_loc   = loc

    logger.debug(
        "Beat %d → Scene %d: coarse=%.3f refined=%.3f @%.3fs",
        beat.beat_id, scene.scene_id, best_score, refined_score, refined_t,
    )
    return refined_t, refined_score, refined_loc


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def run_deep_scan(
    beat: TrailerBeat,
    candidates: Sequence[VibeHit],
    scenes_by_id: dict[int, Scene],
    cfg: AppConfig,
) -> MatchResult | None:
    """
    Phase 2 Deep Scan: iterate over Vibe Check candidates and template-match.

    Args:
        beat:          The trailer beat to source.
        candidates:    Ranked VibeHit list from Phase 1 (best first).
        scenes_by_id:  Lookup dict: scene_id → Scene.
        cfg:           Application configuration.

    Returns:
        The best MatchResult above threshold, or None if no match found.
    """
    proxy_w = cfg.video.proxy_width
    proxy_h = cfg.video.proxy_height

    template = _prepare_template(beat, cfg, proxy_w, proxy_h)
    if template is None:
        return None

    best_result: MatchResult | None = None

    for vibe_hit in candidates:
        scene = scenes_by_id.get(vibe_hit.scene_id)
        if scene is None:
            logger.warning("VibeHit references unknown scene_id=%d", vibe_hit.scene_id)
            continue

        hit = scan_scene(beat, scene, template, cfg)
        if hit is None:
            continue

        in_point_s, match_score, match_loc = hit

        # Frame number: approximate via FPS (refined later if needed)
        from src.cv.frame_extractor import get_video_info
        info = get_video_info(scene.source_path)
        fps  = float(info["fps"]) or 24.0
        in_point_frame = int(in_point_s * fps)

        candidate_result = MatchResult(
            beat_id=beat.beat_id,
            scene_id=scene.scene_id,
            source_path=scene.source_path,
            in_point_s=in_point_s,
            out_point_s=in_point_s + beat.duration_s,
            in_point_frame=in_point_frame,
            match_score=match_score,
            match_location=match_loc,
            vibe_hit=vibe_hit,
        )

        if best_result is None or match_score > best_result.match_score:
            best_result = candidate_result

        # Early exit: if score is very high, no need to check other candidates
        if match_score >= 0.90:
            logger.info(
                "Beat %d: early-exit match (score=%.3f) in scene %d @%.3fs",
                beat.beat_id, match_score, scene.scene_id, in_point_s,
            )
            break

    if best_result:
        logger.info("Beat %d → MATCH scene=%d score=%.3f in=%.3fs",
                    beat.beat_id, best_result.scene_id,
                    best_result.match_score, best_result.in_point_s)
    else:
        logger.warning("Beat %d → NO MATCH found in %d candidates.",
                       beat.beat_id, len(candidates))

    return best_result
