"""
src/pipeline/matcher.py — Top-level CV matching orchestrator

This is the single entry point for the full 2-phase CV pipeline:

  Phase 0: Load / build scene index (PySceneDetect + fingerprinting)
  Phase 1: Vibe Check — histogram + pHash filter → Top-K candidates per beat
  Phase 2: Deep Scan — template matching → frame-accurate MatchResult per beat

Usage:
    from src.core.config import load_config
    from src.pipeline.matcher import run_matching

    cfg     = load_config()
    beats   = [...]          # list[TrailerBeat] from trailer analysis
    results = run_matching(cfg, beats)
"""

from __future__ import annotations

import logging
from typing import Sequence

from src.core.config import AppConfig
from src.core.models import MatchResult, Scene, TrailerBeat

logger = logging.getLogger(__name__)
SeedPoint = float | tuple[float, float]


def _scene_seed_points(scene: Scene, max_points: int) -> list[float]:
    if max_points <= 1 or scene.duration_s <= 0:
        return [scene.start_s]
    usable_end = max(scene.start_s, scene.end_s - 0.2)
    if usable_end <= scene.start_s:
        return [scene.start_s]
    step = (usable_end - scene.start_s) / max(1, max_points - 1)
    return [scene.start_s + step * idx for idx in range(max_points)]


def _build_scene_seed_in_points(
    beats: Sequence[TrailerBeat],
    scenes: Sequence[Scene],
    cfg: AppConfig,
) -> dict[int, list[float]]:
    from src.cv.vibe_check import run_vibe_check

    scenes_by_id = {scene.scene_id: scene for scene in scenes}
    seeds: dict[int, list[float]] = {}
    for beat in beats:
        hits = run_vibe_check(
            beat,
            scenes,
            top_k=cfg.cv.deep_scan.scene_seed_top_k,
            hist_method=cfg.cv.vibe_check.hist_compare_method,
            phash_max_distance=64,
        )
        points: list[float] = []
        for hit in hits:
            scene = scenes_by_id.get(hit.scene_id)
            if scene is None:
                continue
            points.extend(_scene_seed_points(scene, cfg.cv.deep_scan.scene_seed_points_per_scene))
        if points:
            seeds[beat.beat_id] = sorted({round(max(0.0, p), 3) for p in points})
            logger.info(
                "Beat %d: added %d scene-level seed candidates from %d source scenes.",
                beat.beat_id,
                len(seeds[beat.beat_id]),
                len(hits),
            )
    return seeds


def _merge_seed_in_points(
    *seed_maps: dict[int, Sequence[SeedPoint]] | None,
) -> dict[int, list[SeedPoint]]:
    merged: dict[int, dict[float, float | None]] = {}
    for seed_map in seed_maps:
        if not seed_map:
            continue
        for beat_id, points in seed_map.items():
            beat_points = merged.setdefault(beat_id, {})
            for point in points:
                if isinstance(point, tuple):
                    t_sec = round(max(0.0, float(point[0])), 3)
                    score = float(point[1])
                else:
                    t_sec = round(max(0.0, float(point)), 3)
                    score = None
                old_score = beat_points.get(t_sec)
                if old_score is None:
                    beat_points[t_sec] = score
                elif score is not None:
                    beat_points[t_sec] = max(old_score, score)

    result: dict[int, list[SeedPoint]] = {}
    for beat_id, points in merged.items():
        result[beat_id] = [
            (t_sec, score) if score is not None else t_sec
            for t_sec, score in sorted(points.items())
        ]
    return result


# ---------------------------------------------------------------------------
# Beat fingerprinting
# ---------------------------------------------------------------------------

def fingerprint_beats(
    beats: Sequence[TrailerBeat],
    cfg: AppConfig,
) -> list[TrailerBeat]:
    """
    Enrich every TrailerBeat with its visual fingerprint (histogram + pHash).

    Extracts the midpoint frame from the reference trailer and fingerprints it
    using the same Text-Safe Crop parameters as the scene indexer.

    Args:
        beats: TrailerBeat list (fingerprints will be None initially).
        cfg:   Application configuration.

    Returns:
        New list of TrailerBeat objects with luma_hist, sat_hist, phash set.
    """
    from dataclasses import replace
    from src.cv.fingerprinting import fingerprint_frame
    from src.cv.frame_extractor import grab_frame_at_path

    vc_cfg   = cfg.cv.vibe_check
    enriched: list[TrailerBeat] = []

    for beat in beats:
        frame = grab_frame_at_path(beat.trailer_path, beat.midpoint_s)
        if frame is None:
            logger.warning("Beat %d: cannot decode midpoint frame, leaving unfingerpinted.", beat.beat_id)
            enriched.append(beat)
            continue

        luma_b, sat_b, phash = fingerprint_frame(frame, vc_cfg)
        enriched.append(replace(beat, luma_hist=luma_b, sat_hist=sat_b, phash=phash))

    logger.info("Fingerprinted %d / %d beats.", sum(1 for b in enriched if b.phash), len(beats))
    return enriched


# ---------------------------------------------------------------------------
# Main pipeline entry point
# ---------------------------------------------------------------------------

def run_matching(
    cfg: AppConfig,
    beats: Sequence[TrailerBeat],
    force_reindex: bool = False,
    seed_in_points: dict[int, Sequence[SeedPoint]] | None = None,
) -> list[MatchResult]:
    """
    Execute the full 2-phase CV matching pipeline.

    Args:
        cfg:           Application configuration (loaded from config.toml).
        beats:         All trailer beats to source (must have trailer_path set).
        force_reindex: If True, ignore the scene cache and re-run PySceneDetect.

    Returns:
        List of MatchResult, one per beat (unmatched beats are omitted).
        Results are in the same order as the input beats.
    """
    from src.cv.scene_indexer import build_scene_index

    logger.info("=" * 60)
    logger.info("AI Trailer Generator v2 — CV Matching Pipeline")
    logger.info("Source : %s", cfg.paths.source_movie.name)
    logger.info("Trailer: %s", cfg.paths.reference_trailer.name)
    logger.info("Beats  : %d", len(beats))
    logger.info("=" * 60)
    if not beats:
        logger.info("No beats requested; skipping scene and global scans.")
        return []

    # ------------------------------------------------------------------
    # Phase 0: Scene index
    # ------------------------------------------------------------------
    logger.info("[Phase 0] Building scene index …")
    scenes: list[Scene] = build_scene_index(cfg, force_reindex=force_reindex)
    scenes_by_id: dict[int, Scene] = {s.scene_id: s for s in scenes}
    logger.info("[Phase 0] %d scenes indexed.", len(scenes))

    # ------------------------------------------------------------------
    # Phase 0b: Fingerprint the beats
    # ------------------------------------------------------------------
    logger.info("[Phase 0b] Fingerprinting %d trailer beats …", len(beats))
    beats = fingerprint_beats(beats, cfg)

    # ------------------------------------------------------------------
    # Phase 1 & 2: Global Scan (bypasses Scene Indexer / Vibe Check entirely)
    # ------------------------------------------------------------------
    logger.info("[Phase 1 & 2] Running FFmpeg Global Scan for %d beats ...", len(beats))
    from src.cv.global_scan import run_global_scan
    
    scene_seed_in_points = _build_scene_seed_in_points(beats, scenes, cfg)
    vision_seed_in_points = {}
    if cfg.vision.enabled:
        try:
            from src.llm.vision_cache import build_vision_seed_in_points

            vision_seed_in_points = build_vision_seed_in_points(beats, scenes, cfg)
        except Exception as exc:
            logger.error("Vision seeding failed: %s — continuing with CV-only seeds.", exc)
    results = run_global_scan(
        beats,
        cfg,
        scenes=scenes,
        seed_in_points=_merge_seed_in_points(seed_in_points, scene_seed_in_points, vision_seed_in_points),
    )

    logger.info("[Phase 1 & 2] Done. %d / %d beats matched.", len(results), len(beats))
    logger.info("=" * 60)

    return results


# ---------------------------------------------------------------------------
# Convenience: build an EditTimeline from match results
# ---------------------------------------------------------------------------

def build_timeline(
    beats: Sequence[TrailerBeat],
    results: Sequence[MatchResult],
    cfg: AppConfig,
) -> "src.core.models.EditTimeline":  # type: ignore[name-defined]
    """
    Combine beats + match results into an ordered EditTimeline.

    Unmatched beats are skipped; timeline positions are computed
    sequentially from the usable source-match durations.

    Args:
        beats:   All trailer beats (defines order + durations).
        results: MatchResult list from run_matching().
        cfg:     Application configuration.

    Returns:
        EditTimeline ready for FCPXML / EDL export.
    """
    from src.core.models import EditClip, EditTimeline

    results_by_beat: dict[int, MatchResult] = {r.beat_id: r for r in results}

    clips: list[EditClip] = []
    cursor = 0.0

    for beat in beats:
        match = results_by_beat.get(beat.beat_id)
        if match is None:
            logger.warning("Beat %d has no match — gap in timeline.", beat.beat_id)
            cursor += beat.duration_s
            continue

        match_duration = max(0.0, match.duration_s)
        source_duration = min(beat.duration_s, match_duration) if match_duration > 0 else beat.duration_s
        trailer_tail_s = max(0.0, beat.duration_s - source_duration)
        if trailer_tail_s > 0:
            logger.warning(
                "Beat %d uses %.2fs source + %.2fs generated trailer tail.",
                beat.beat_id,
                source_duration,
                trailer_tail_s,
            )

        clip = EditClip(
            clip_index=len(clips),
            beat=beat,
            match=match,
            timeline_start_s=cursor,
            timeline_end_s=cursor + beat.duration_s,
            source_duration_s=source_duration,
            trailer_tail_s=trailer_tail_s,
        )
        clips.append(clip)
        cursor += beat.duration_s

    timeline = EditTimeline(
        title=cfg.paths.reference_trailer.stem,
        frame_rate=cfg.export.edl_frame_rate,
        clips=tuple(clips),
    )

    logger.info(
        "Timeline built: %d clips, total duration %.2fs",
        timeline.clip_count, timeline.total_duration_s,
    )
    return timeline
