"""
src/cv/scene_indexer.py — Source-movie scene segmentation + fingerprinting

Responsibility:
  1. Run PySceneDetect on the source movie → list of raw scene boundaries
  2. For each scene, extract the midpoint frame and fingerprint it
  3. Optionally run Whisper dialogue on each scene (injected as dependency)
  4. Persist results to .cache/ as JSON for fast re-runs

Returns: list[Scene] with luma_hist, sat_hist, phash populated.
"""

from __future__ import annotations

import json
import logging
import pickle
from pathlib import Path
from typing import Callable, Sequence

import numpy as np

from src.core.config import AppConfig
from src.core.models import Scene
from src.cv.fingerprinting import fingerprint_frame
from src.cv.frame_extractor import grab_midpoint_frame, open_video

logger = logging.getLogger(__name__)

# Type alias for an optional dialogue-injection callback
DialogueCallback = Callable[[Scene], Scene]


# ---------------------------------------------------------------------------
# Cache helpers
# ---------------------------------------------------------------------------

def _cache_path(cfg: AppConfig) -> Path:
    p = cfg.paths.cache_dir / "scene_index.json"
    p.parent.mkdir(parents=True, exist_ok=True)
    return p


def _scene_to_dict(s: Scene) -> dict:
    return {
        "scene_id":    s.scene_id,
        "source_path": str(s.source_path),
        "start_s":     s.start_s,
        "end_s":       s.end_s,
        "start_frame": s.start_frame,
        "end_frame":   s.end_frame,
        # histograms serialised as hex so JSON can hold them
        "luma_hist":   s.luma_hist.hex() if s.luma_hist else None,
        "sat_hist":    s.sat_hist.hex()  if s.sat_hist  else None,
        "phash":       s.phash,
    }


def _scene_from_dict(d: dict) -> Scene:
    return Scene(
        scene_id=d["scene_id"],
        source_path=Path(d["source_path"]),
        start_s=d["start_s"],
        end_s=d["end_s"],
        start_frame=d["start_frame"],
        end_frame=d["end_frame"],
        luma_hist=bytes.fromhex(d["luma_hist"]) if d.get("luma_hist") else None,
        sat_hist= bytes.fromhex(d["sat_hist"])  if d.get("sat_hist")  else None,
        phash=d.get("phash"),
    )


def _save_cache(scenes: list[Scene], cfg: AppConfig) -> None:
    data = [_scene_to_dict(s) for s in scenes]
    _cache_path(cfg).write_text(json.dumps(data, indent=2), encoding="utf-8")
    logger.info("Scene index cached → %s (%d scenes)", _cache_path(cfg), len(scenes))


def _load_cache(cfg: AppConfig) -> list[Scene] | None:
    p = _cache_path(cfg)
    if not p.exists():
        return None
    try:
        data = json.loads(p.read_text(encoding="utf-8"))
        scenes = [_scene_from_dict(d) for d in data]
        logger.info("Loaded %d scenes from cache (%s)", len(scenes), p)
        return scenes
    except Exception as exc:
        logger.warning("Cache corrupt, re-indexing: %s", exc)
        return None


# ---------------------------------------------------------------------------
# PySceneDetect integration
# ---------------------------------------------------------------------------

def _detect_scenes_pyscenedetect(cfg: AppConfig) -> list[tuple[float, float, int, int]]:
    """
    Run PySceneDetect ContentDetector on the source movie.

    Returns:
        List of (start_s, end_s, start_frame, end_frame) tuples.
    """
    try:
        from scenedetect import open_video as sd_open_video, SceneManager
        from scenedetect.detectors import ContentDetector
    except ImportError:
        raise ImportError(
            "scenedetect is not installed. Run: pip install scenedetect[opencv]"
        )

    video = sd_open_video(str(cfg.paths.source_movie))
    manager = SceneManager()
    manager.add_detector(
        ContentDetector(
            threshold=cfg.scene_detection.content_threshold,
            min_scene_len=int(
                cfg.scene_detection.min_scene_duration_s
                * video.frame_rate
            ),
        )
    )

    logger.info("Detecting scenes in %s …", cfg.paths.source_movie.name)
    manager.detect_scenes(video=video, show_progress=True)

    raw = manager.get_scene_list()
    result: list[tuple[float, float, int, int]] = []
    for start_tc, end_tc in raw:
        result.append((
            start_tc.get_seconds(),
            end_tc.get_seconds(),
            start_tc.get_frames(),
            end_tc.get_frames(),
        ))

    logger.info("PySceneDetect found %d scenes.", len(result))
    return result


# ---------------------------------------------------------------------------
# Fingerprint enrichment
# ---------------------------------------------------------------------------

def _fingerprint_scenes(
    raw_scenes: list[tuple[float, float, int, int]],
    cfg: AppConfig,
) -> list[Scene]:
    """
    For each raw scene boundary, extract the midpoint frame and fingerprint it.
    """
    scenes: list[Scene] = []
    vc_cfg = cfg.cv.vibe_check

    logger.info("Fingerprinting %d scenes …", len(raw_scenes))

    with open_video(cfg.paths.source_movie) as cap:
        for idx, (start_s, end_s, start_frame, end_frame) in enumerate(raw_scenes):
            frame = grab_midpoint_frame(cap, start_s, end_s)

            if frame is None:
                logger.warning("Scene %d: midpoint frame decode failed, skipping fingerprint.", idx)
                scenes.append(Scene(
                    scene_id=idx,
                    source_path=cfg.paths.source_movie,
                    start_s=start_s, end_s=end_s,
                    start_frame=start_frame, end_frame=end_frame,
                ))
                continue

            luma_bytes, sat_bytes, phash_hex = fingerprint_frame(frame, vc_cfg)

            scenes.append(Scene(
                scene_id=idx,
                source_path=cfg.paths.source_movie,
                start_s=start_s, end_s=end_s,
                start_frame=start_frame, end_frame=end_frame,
                luma_hist=luma_bytes,
                sat_hist=sat_bytes,
                phash=phash_hex,
            ))

            if (idx + 1) % 50 == 0:
                logger.info("  … %d / %d scenes fingerprinted", idx + 1, len(raw_scenes))

    return scenes


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

def build_scene_index(
    cfg: AppConfig,
    force_reindex: bool = False,
    dialogue_callback: DialogueCallback | None = None,
) -> list[Scene]:
    """
    Build (or load from cache) the full scene index for the source movie.

    Steps:
      1. Load from .cache/scene_index.json if available and force_reindex=False.
      2. Otherwise: detect scenes via PySceneDetect → fingerprint → cache.
      3. Optionally enrich each scene with dialogue via dialogue_callback.

    Args:
        cfg:               Application configuration.
        force_reindex:     Ignore cache and re-run detection + fingerprinting.
        dialogue_callback: Optional function Scene → Scene that adds dialogue.
                           Injected here so this module stays audio-free.

    Returns:
        List of Scene objects with fingerprints populated.
    """
    if not force_reindex:
        cached = _load_cache(cfg)
        if cached is not None:
            if dialogue_callback:
                cached = [dialogue_callback(s) for s in cached]
            return cached

    raw = _detect_scenes_pyscenedetect(cfg)
    scenes = _fingerprint_scenes(raw, cfg)
    _save_cache(scenes, cfg)

    if dialogue_callback:
        scenes = [dialogue_callback(s) for s in scenes]

    return scenes
