"""
src/core/models.py — Canonical data models for AI Trailer Generator v2

Rules:
  - Every model is a frozen dataclass (immutable after creation).
  - All fields are strictly typed; no bare dicts or untyped lists.
  - Seconds are always float; frame numbers are always int.
  - Confidence scores live in [0.0, 1.0].
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum, auto
from pathlib import Path
from typing import Optional


# ===========================================================================
# Enumerations
# ===========================================================================

class MatchMethod(Enum):
    """CV template matching method (mirrors cv2.TM_* constants)."""
    TM_SQDIFF         = 0
    TM_SQDIFF_NORMED  = 1
    TM_CCORR          = 2
    TM_CCORR_NORMED   = 3
    TM_CCOEFF         = 4
    TM_CCOEFF_NORMED  = 5


class BeatType(Enum):
    """Narrative role of a trailer beat (for dramaturgy / LLM use only)."""
    HOOK        = auto()   # Opening attention grabber
    SETUP       = auto()   # World / character introduction
    CONFLICT    = auto()   # Inciting incident / rising tension
    CLIMAX      = auto()   # Peak action / emotion
    RESOLUTION  = auto()   # Cool-down / tagline
    UNKNOWN     = auto()


class ExportFormat(Enum):
    FCPXML = "fcpxml"
    EDL    = "edl"
    BOTH   = "both"


# ===========================================================================
# Phase 0 — Source-movie scene index
# ===========================================================================

@dataclass(frozen=True)
class DialogueLine:
    """Single transcribed line from Whisper output."""
    start_s: float       # onset in seconds
    end_s:   float       # offset in seconds
    text:    str         # verbatim transcript
    speaker: Optional[str] = None  # diarisation label if available

    @property
    def duration_s(self) -> float:
        return self.end_s - self.start_s


@dataclass(frozen=True)
class Scene:
    """
    One detected scene in the source movie.

    Produced by PySceneDetect; enriched by Whisper dialogue and
    (optionally) perceptual hashes during the Vibe Check phase.
    """
    scene_id:       int          # zero-based index in source movie
    source_path:    Path         # absolute path to the source video file
    start_s:        float        # scene start in seconds
    end_s:          float        # scene end   in seconds
    start_frame:    int          # first frame number
    end_frame:      int          # last  frame number

    # Populated after Vibe Check fingerprinting
    luma_hist:      Optional[bytes]  = None  # serialised np.ndarray (pickle)
    sat_hist:       Optional[bytes]  = None
    phash:          Optional[str]    = None  # 64-bit hex string

    # Populated after Whisper pass
    dialogue:       tuple[DialogueLine, ...] = field(default_factory=tuple)

    @property
    def duration_s(self) -> float:
        return self.end_s - self.start_s

    @property
    def midpoint_s(self) -> float:
        return self.start_s + self.duration_s / 2.0

    def __repr__(self) -> str:
        return (
            f"Scene(id={self.scene_id}, "
            f"{self.start_s:.2f}s–{self.end_s:.2f}s, "
            f"dur={self.duration_s:.2f}s)"
        )


# ===========================================================================
# Phase 1 — Reference-trailer beat
# ===========================================================================

@dataclass(frozen=True)
class TrailerBeat:
    """
    One cut / segment in the reference trailer.

    The 'beat' is the atomic unit of a trailer:  it maps exactly to one
    clip that will later be sourced from the original movie.
    """
    beat_id:        int
    trailer_path:   Path
    start_s:        float
    end_s:          float
    start_frame:    int
    end_frame:      int

    beat_type:      BeatType = BeatType.UNKNOWN  # set by LLM dramaturgy pass

    # Visual fingerprints of the *middle* frame (populated by CV pipeline)
    luma_hist:      Optional[bytes] = None
    sat_hist:       Optional[bytes] = None
    phash:          Optional[str]   = None

    # Dialogue extracted from this beat
    dialogue:       tuple[DialogueLine, ...] = field(default_factory=tuple)

    @property
    def duration_s(self) -> float:
        return self.end_s - self.start_s

    @property
    def midpoint_s(self) -> float:
        return self.start_s + self.duration_s / 2.0

    def __repr__(self) -> str:
        return (
            f"TrailerBeat(id={self.beat_id}, "
            f"{self.beat_type.name}, "
            f"{self.start_s:.2f}s–{self.end_s:.2f}s)"
        )


# ===========================================================================
# Phase 2 — CV match result
# ===========================================================================

@dataclass(frozen=True)
class VibeHit:
    """
    Intermediate result from Phase 1 (Vibe Check — histogram/pHash).

    Represents a *candidate* scene that passed the coarse filter.
    Not yet a confirmed match; forwarded to Deep Scan.
    """
    beat_id:            int
    scene_id:           int
    hist_score:         float   # histogram similarity [0.0, 1.0] (CORREL method)
    phash_distance:     int     # Hamming distance [0, 64]; lower = more similar
    combined_score:     float   # weighted aggregate used for ranking


@dataclass(frozen=True)
class MatchSegment:
    """
    One source-backed visual island inside a trailer beat.

    Some trailer beats contain multiple shots separated by fades/title frames.
    A single continuous source in/out cannot represent those beats accurately.
    """
    trailer_offset_s:   float
    duration_s:         float
    scene_id:           int
    in_point_s:         float
    out_point_s:        float
    match_score:        float
    is_confirmed:       bool = True


@dataclass(frozen=True)
class MatchResult:
    """
    Final, confirmed match from Phase 2 (Deep Scan — template matching).

    One MatchResult per TrailerBeat: the best frame-accurate hit found
    inside the source movie.
    """
    beat_id:            int       # which trailer beat was matched
    scene_id:           int       # which source scene contains the match
    source_path:        Path      # absolute path to source video

    # Frame-accurate in-point / out-point in the SOURCE movie
    in_point_s:         float     # matched frame onset in source seconds
    out_point_s:        float     # computed out-point (in_point + beat duration)
    in_point_frame:     int       # matched frame number in source movie

    # Match quality
    match_score:        float     # cv2.matchTemplate peak value [0.0, 1.0]
    match_location:     tuple[int, int] = field(default_factory=lambda: (0, 0))
    # (x, y) pixel location of the best match within the source frame

    # Provenance
    vibe_hit:           Optional[VibeHit] = None  # the candidate that led here
    is_confirmed:       bool = True
    segments:           tuple[MatchSegment, ...] = field(default_factory=tuple)

    @property
    def duration_s(self) -> float:
        return self.out_point_s - self.in_point_s

    def __repr__(self) -> str:
        return (
            f"MatchResult(beat={self.beat_id} → scene={self.scene_id}, "
            f"in={self.in_point_s:.3f}s, score={self.match_score:.3f})"
        )


# ===========================================================================
# Phase 3 — Edit timeline (pre-export)
# ===========================================================================

@dataclass(frozen=True)
class EditClip:
    """
    One clip on the final edit timeline, ready for FCPXML / EDL export.

    Combines beat dramaturgy + the CV-confirmed source in/out points.
    """
    clip_index:         int        # position on the timeline (0-based)
    beat:               TrailerBeat
    match:              MatchResult

    # Timeline position (in the OUTPUT trailer)
    timeline_start_s:   float
    timeline_end_s:     float
    source_duration_s:  float | None = None
    trailer_tail_s:     float = 0.0

    # Optional audio override (e.g. VO or music)
    audio_path:         Optional[Path] = None
    audio_offset_s:     float = 0.0

    @property
    def timeline_duration_s(self) -> float:
        return self.timeline_end_s - self.timeline_start_s

    @property
    def source_timeline_duration_s(self) -> float:
        if self.source_duration_s is not None:
            return max(0.0, self.source_duration_s)
        return self.timeline_duration_s

    def __repr__(self) -> str:
        return (
            f"EditClip(#{self.clip_index}, "
            f"tl={self.timeline_start_s:.2f}s–{self.timeline_end_s:.2f}s, "
            f"src={self.match.in_point_s:.3f}s)"
        )


@dataclass(frozen=True)
class EditTimeline:
    """
    The complete ordered sequence of EditClips that forms the trailer.

    Passed to the export layer (FCPXML / EDL writer).
    """
    title:          str
    frame_rate:     float                   # e.g. 23.976
    clips:          tuple[EditClip, ...]    # ordered by clip_index

    @property
    def total_duration_s(self) -> float:
        if not self.clips:
            return 0.0
        last = max(self.clips, key=lambda c: c.timeline_end_s)
        return last.timeline_end_s

    @property
    def clip_count(self) -> int:
        return len(self.clips)
