"""
src/cv/vibe_check.py — Phase 1: Scene-level histogram / pHash filter

Responsibility:
  Given ONE TrailerBeat (with pre-computed fingerprints) and a list of
  source Scenes (also fingerprinted), return the Top-K candidates ranked
  by a combined histogram + pHash score.

This module contains ZERO file I/O and ZERO frame decoding — those live
in the pipeline layer. Input = model objects, output = sorted VibeHit list.
"""

from __future__ import annotations

import logging
from dataclasses import replace
from typing import Sequence

import cv2
import numpy as np

from src.core.models import Scene, TrailerBeat, VibeHit
from src.cv.fingerprinting import bytes_to_hist, phash_distance

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Scoring
# ---------------------------------------------------------------------------

# Weight applied to histogram score vs pHash score in the combined metric.
# pHash gets less weight because it's sensitive to text overlays on source.
_HIST_WEIGHT  = 0.70
_PHASH_WEIGHT = 0.30
_PHASH_MAX_BITS = 64  # maximum possible Hamming distance


def _hist_combined_score(
    beat: TrailerBeat,
    scene: Scene,
    hist_method: int,
) -> float:
    """
    Average CORREL score of luma + saturation histograms.

    Returns a value in [-1, 1] (CORREL) or [0, 1] depending on method.
    Higher is always more similar (we invert BHATTACHARYYA if needed).
    """
    if beat.luma_hist is None or scene.luma_hist is None:
        return 0.0
    if beat.sat_hist is None or scene.sat_hist is None:
        return 0.0

    luma_score = cv2.compareHist(
        bytes_to_hist(beat.luma_hist),
        bytes_to_hist(scene.luma_hist),
        hist_method,
    )
    sat_score = cv2.compareHist(
        bytes_to_hist(beat.sat_hist),
        bytes_to_hist(scene.sat_hist),
        hist_method,
    )

    # Normalise BHATTACHARYYA to [0, 1] similarity (invert distance)
    if hist_method == cv2.HISTCMP_BHATTACHARYYA:
        luma_score = 1.0 - float(luma_score)
        sat_score  = 1.0 - float(sat_score)

    return float((luma_score + sat_score) / 2.0)


def _phash_score(beat: TrailerBeat, scene: Scene) -> float:
    """
    Convert Hamming distance to a [0, 1] similarity score.

    0 Hamming distance → 1.0 (identical)
    64 Hamming distance → 0.0 (completely different)
    """
    if beat.phash is None or scene.phash is None:
        return 0.0
    dist = phash_distance(beat.phash, scene.phash)
    return 1.0 - (dist / _PHASH_MAX_BITS)


def _combined_score(
    beat: TrailerBeat,
    scene: Scene,
    hist_method: int,
) -> float:
    """Weighted aggregate of histogram + pHash similarity."""
    hist  = _hist_combined_score(beat, scene, hist_method)
    phash = _phash_score(beat, scene)
    return _HIST_WEIGHT * hist + _PHASH_WEIGHT * phash


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

def run_vibe_check(
    beat: TrailerBeat,
    scenes: Sequence[Scene],
    top_k: int,
    hist_method: int,
    phash_max_distance: int,
) -> list[VibeHit]:
    """
    Phase 1: Score all source scenes against one trailer beat and return
    the top-K candidates for Deep Scan.

    Args:
        beat:               The trailer beat to match (must have fingerprints).
        scenes:             All detected scenes from the source movie.
        top_k:              Maximum number of candidates to return.
        hist_method:        cv2.HISTCMP_* constant (e.g. 0 = CORREL).
        phash_max_distance: Scenes with pHash Hamming distance > this value
                            are excluded before ranking (hard filter).

    Returns:
        List of VibeHit, sorted by combined_score descending, length ≤ top_k.
        Empty list if beat has no fingerprints or no scenes pass the filter.
    """
    if beat.luma_hist is None and beat.phash is None:
        logger.warning(
            "Beat %d has no fingerprints — skipping Vibe Check.", beat.beat_id
        )
        return []

    candidates: list[VibeHit] = []

    for scene in scenes:
        # Hard pHash filter: skip scenes that are too visually distant
        if beat.phash and scene.phash:
            dist = phash_distance(beat.phash, scene.phash)
            if dist > phash_max_distance:
                continue  # fast rejection — avoids full histogram compare

        hist  = _hist_combined_score(beat, scene, hist_method)
        phash = _phash_score(beat, scene)
        combined = _HIST_WEIGHT * hist + _PHASH_WEIGHT * phash

        candidates.append(VibeHit(
            beat_id=beat.beat_id,
            scene_id=scene.scene_id,
            hist_score=round(hist, 4),
            phash_distance=(
                phash_distance(beat.phash, scene.phash)
                if beat.phash and scene.phash
                else _PHASH_MAX_BITS
            ),
            combined_score=round(combined, 4),
        ))

    # Sort by combined score, descending; return top-K
    candidates.sort(key=lambda h: h.combined_score, reverse=True)
    top = candidates[:top_k]

    logger.info(
        "Vibe Check beat=%d: %d scenes scored, %d candidates forwarded to Deep Scan. "
        "Best score: %.3f (scene %s)",
        beat.beat_id,
        len(candidates),
        len(top),
        top[0].combined_score if top else 0.0,
        top[0].scene_id if top else "—",
    )

    return top


def batch_vibe_check(
    beats: Sequence[TrailerBeat],
    scenes: Sequence[Scene],
    top_k: int,
    hist_method: int,
    phash_max_distance: int,
) -> dict[int, list[VibeHit]]:
    """
    Run Vibe Check for every beat and return a mapping beat_id → [VibeHit].

    Convenience wrapper for the pipeline layer.
    """
    return {
        beat.beat_id: run_vibe_check(
            beat, scenes, top_k, hist_method, phash_max_distance
        )
        for beat in beats
    }
