"""Data models for GnommoEditor pipeline."""

from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional, Union


@dataclass
class CutoutDefinition:
    """Definition of a named zone for placing video content.

    All positioning values support both pixels (int) and percentages (str like "50%").
    Percentage values are stored as floats (0.0-1.0) with pixel value set to -1.

    Videos placed in cutouts are cropped to fit the cutout dimensions.
    """

    x: int  # in pixels, or -1 for percentage-based
    y: int  # in pixels, or -1 for percentage-based
    height: int  # in pixels, or -1 for percentage-based
    width: int = (
        -1
    )  # in pixels, or -1 for percentage-based (defaults to height for square)
    x_percent: float = 0.0  # percentage (0.0-1.0) if x is -1
    y_percent: float = 0.0  # percentage (0.0-1.0) if y is -1
    height_percent: float = 0.0  # percentage (0.0-1.0) if height is -1
    width_percent: float = 0.0  # percentage (0.0-1.0) if width is -1


# Backwards compatibility alias
TalkingHeadConfig = CutoutDefinition


@dataclass
class ProjectConfig:
    """Global project configuration from project.json."""

    resolution: tuple[int, int]
    fps: int
    default_slide_type: str
    cutouts: dict[str, CutoutDefinition] = field(
        default_factory=dict
    )  # Named zones for video placement
    default_filters: dict[str, list[dict]] = field(
        default_factory=dict
    )  # Named filter presets that can be referenced in videos.json
    background: str = ""  # Background image or video path (in shared_assets/)
    background_video: str = ""  # Deprecated: use background instead
    slides_path: str = "slides.json"  # path to slides.json relative to project
    videos_path: str = "videos.json"  # path to videos.json relative to project
    audio_path: str = "audio.json"  # path to audio.json relative to project
    transcript_path: Optional[str] = None  # path to transcript.json relative to project (always saved locally)
    audio_source: Optional[str] = None  # defaults to talking head
    main_video: Optional[
        Union[str, list]
    ] = None  # ID(s) of main video(s) - array for multi-segment narration
    gnommo_scratch: Optional[
        str
    ] = None  # directory for intermediate files (e.g., external SSD)
    process_cache: Optional[
        str
    ] = None  # external directory for processed/combined outputs (saves laptop disk space)
    default_begin: float = 0.0  # Trim this many seconds from the start of each segment (if no explicit begin/skip)
    default_end_trim: float = 0.0  # Trim this many seconds from the end of each segment (if no explicit end/take)
    # Outro sequence - plays after narration ends (not marker-triggered)
    outro: list[str] = field(
        default_factory=list
    )  # List of video IDs to play in sequence after narration
    # YouTube description fields
    description: str = ""  # Video description text for YouTube
    footer: str = ""  # Footer text (social links, subscribe CTA, etc.)
    output_video: str = (
        ""  # Output filename (e.g. "DISC_INT3.mp4"); placed in out/ or out/<res>/
    )


@dataclass
class SlideDefinition:
    """Definition of a single slide from slides.json."""

    image: str
    type: str  # "fullscreen" | "square"


@dataclass
class ChromaKeyConfig:
    """Configuration for chroma key (green screen) filter."""

    color: tuple[int, int, int] = (0, 255, 0)  # RGB color to key out
    similarity: float = (
        0.4  # Color similarity threshold (0.0-1.0), higher = more aggressive
    )
    blend: float = 0.08  # Edge blend/feathering (0.0-1.0), lower = tighter edges
    spill: float = 0.1  # Spill suppression amount (0.0-1.0)
    edge_erode: int = 0  # Pixels to erode from alpha edge (0-5), removes green fringe
    # Color protection - restore opacity for colors that shouldn't be keyed
    protect_color: tuple[int, int, int] = None  # RGB color to protect from keying
    protect_tolerance: float = (
        0.15  # How much variation from protect_color to allow (0-1)
    )


@dataclass
class GnommoKeyConfig:
    """Configuration for gnommokey filter - Keylight-style color-difference keyer.

    Uses YCbCr color-difference keying (like Keylight/Ultimatte) instead of
    simple Euclidean distance. This handles lighting variation much better
    than basic chromakey.
    """

    # Screen color (the green/blue screen color to key out)
    screen_color: tuple[int, int, int] = (0, 177, 64)  # RGB of the screen

    # Key extraction strength (default 100, higher = more aggressive)
    # Values 80-150 are typical. Maps to Keylight's Screen Gain.
    screen_gain: float = 100.0

    # Balance between chrominance and luminance in key calculation (0-100)
    # 0 = pure color-difference, 100 = luminance weighted
    # Maps to Keylight's Screen Balance.
    screen_balance: float = 50.0

    # Alpha/matte adjustments
    clip_black: float = 0.0  # Crush blacks (0-100). Higher = more transparent areas
    clip_white: float = 100.0  # Crush whites (0-100). Lower = more opaque areas

    # Despill: color to shift green spill toward (RGB)
    # Typical values: skin tone [217, 200, 180] or neutral [200, 200, 200]
    despill_bias: tuple[int, int, int] = None

    # How aggressively to apply despill (0-1)
    despill_strength: float = 0.5

    # Alpha bias: influences edge treatment (RGB)
    # Can help with edge color contamination
    alpha_bias: tuple[int, int, int] = None

    # Luminance protection: pixels with luma above this stay fully opaque (0-255, -1 = off)
    # Use ~220 to protect white objects (headphones, teeth) from being partially keyed.
    protect_luma: int = -1

    # Shadow boost: extra key strength for dark pixels (0.0-5.0, 0 = off)
    # Ramps up key signal proportionally to how dark a pixel is, helping key dark greens
    # without affecting bright foreground areas. Values 1.0-2.0 are typical.
    shadow_boost: float = 0.0

    # Edge refinement
    edge_erode: int = 0  # Pixels to erode from alpha edge (0-5)
    edge_soften: float = 0.0  # Blur the alpha edge (0-5 pixels)


@dataclass
class ColorGradeConfig:
    """Configuration for color grading filter.

    Applies color balance, contrast curves, and saturation adjustments
    while preserving the alpha channel.
    """

    # Color balance (range: -1.0 to 1.0, 0 = no change)
    # Midtones
    rm: float = 0.0  # Red midtones adjustment
    gm: float = 0.0  # Green midtones adjustment
    bm: float = 0.0  # Blue midtones adjustment
    # Highlights
    rh: float = 0.0  # Red highlights adjustment
    gh: float = 0.0  # Green highlights adjustment
    bh: float = 0.0  # Blue highlights adjustment
    # Shadows
    rs: float = 0.0  # Red shadows adjustment
    gs: float = 0.0  # Green shadows adjustment
    bs: float = 0.0  # Blue shadows adjustment

    # Curves preset (none, lighter, darker, increase_contrast, medium_contrast, etc.)
    curves_preset: str = "none"

    # EQ adjustments
    contrast: float = 1.0  # Contrast multiplier (0.0-2.0, 1.0 = no change)
    brightness: float = 0.0  # Brightness adjustment (-1.0 to 1.0, 0 = no change)
    saturation: float = 1.0  # Saturation multiplier (0.0-3.0, 1.0 = no change)

    # Custom curves for lift/gamma/gain control
    # Format: "0/0 0.5/0.56 1/1" means (input/output) control points
    curves_r: str = ""  # Red channel curve
    curves_g: str = ""  # Green channel curve
    curves_b: str = ""  # Blue channel curve
    curves_master: str = ""  # Master (luminance) curve


@dataclass
class EQBand:
    """A single parametric EQ band."""

    freq: float  # Center frequency in Hz
    gain: float  # Gain in dB (negative = cut, positive = boost)
    q: float = 1.0  # Q factor (bandwidth), higher = narrower
    type: str = "peak"  # "peak", "lowshelf", or "highshelf"


@dataclass
class AudioNormalizeConfig:
    """Configuration for audio normalization filter.

    Applies noise reduction, compression, and loudness normalization
    to improve audio quality and consistency.
    """
    enabled: bool = True  # Master switch to enable/disable all audio processing
    # Parametric EQ bands (applied before other processing)
    eq_bands: list[EQBand] = field(default_factory=list)

    # High-pass filter (remove room rumble)
    highpass: float = (
        0.0  # High-pass frequency in Hz (0 = disabled, try 80-120 for voice)
    )

    # Low-pass filter (remove harsh highs)
    lowpass: float = (
        0.0  # Low-pass frequency in Hz (0 = disabled, try 12000-16000 if needed)
    )

    # Room resonance EQ cut (reduce muddy room buildup)
    room_eq: bool = False  # Enable room resonance cut
    room_eq_freq: float = 300.0  # Center frequency for room cut (Hz, typically 200-400)
    room_eq_gain: float = -4.0  # Gain in dB (negative = cut)
    room_eq_width: float = 1.5  # Q/bandwidth (higher = narrower cut)

    # Noise gate (reduce reverb tails during pauses)
    gate: bool = False  # Enable noise gate
    gate_threshold: float = -35.0  # Threshold in dB (signal below this gets attenuated)
    gate_range: float = -20.0  # Attenuation amount in dB when gate is closed
    gate_attack: float = 10.0  # Attack time in ms
    gate_release: float = 150.0  # Release time in ms

    # Neural de-reverb (arnndn filter - very effective but needs model file)
    dereverb_model: str = ""  # Path to RNNoise model file (empty = disabled)
    dereverb_mix: float = (
        0.8  # Mix ratio 0.0-1.0 (1.0 = full effect, 0.8 = preserve some natural room)
    )

    # Noise reduction (afftdn filter)
    denoise: bool = True  # Enable noise reduction
    noise_floor: float = (
        -25.0
    )  # Noise floor in dB (default -25, lower = more aggressive)

    # Compression (acompressor filter)
    compress: bool = True  # Enable dynamic range compression
    threshold: float = -20.0  # Compression threshold in dB
    ratio: float = 4.0  # Compression ratio (4:1 default)
    attack: float = 5.0  # Attack time in ms
    release: float = 50.0  # Release time in ms
    makeup: float = 2.0  # Makeup gain in dB

    # Loudness normalization (loudnorm filter - EBU R128)
    normalize: bool = True  # Enable loudness normalization
    target_lufs: float = (
        -16.0
    )  # Target integrated loudness (YouTube recommends -14 to -16)
    target_lra: float = 11.0  # Target loudness range
    target_tp: float = -1.5  # Target true peak in dB


@dataclass
class FilterConfig:
    """Base configuration for a preprocessing filter."""

    type: str
    # Type-specific config stored in subclasses or as dict


@dataclass
class Attribution:
    """Attribution information for stock footage (e.g., Pexels)."""

    source: str  # Source platform (e.g., "pexels", "pixabay", "unsplash")
    creator: str  # Creator/photographer name
    url: Optional[str] = None  # URL to the original content


@dataclass
class VideoSource:
    """Video source definition from videos.json."""

    source_file: str  # Source video filename (relative to videos.json location or shared_assets/)
    filter: list[dict] = field(default_factory=list)  # List of filter config dicts
    output_file: Optional[
        str
    ] = None  # Path to preprocessed output (relative to videos.json)
    take: Optional[
        float
    ] = None  # Max duration to play (seconds). Default: until next slide or end of clip
    skip: float = 0.0  # Skip this many seconds at start of video (seek point)
    zoom: float = (
        1.0  # Scale factor for video (1.0 = fit to cutout height, >1 = enlarge)
    )
    cutout: Optional[
        str
    ] = None  # Name of cutout to place video in (from project.json cutouts)
    always_visible: bool = False  # If True, video is always shown (like talking head)
    is_shared: bool = False  # If True, source_file is relative to shared_assets/
    pause_narration: float = (
        0.0  # Seconds to pause narration during this video (0 = no pause)
    )
    attribution: Optional[Attribution] = None  # Attribution for stock footage
    use_audio_channels: str = (
        "both"  # Audio channel selection: "both", "left", or "right"
    )
    defer_loudnorm: bool = (
        False  # If True, skip loudnorm during preprocessing (apply after concatenation)
    )
    volume: float = 1.0  # Volume multiplier (1.0=full, >1.0=boost, <1.0=reduce)
    layer: str = "above"  # "above" = on top of slides; "mid" = above narrator/below slides; "below" = behind narrator
    duration: Optional[
        float
    ] = None  # Pre-probed file duration in seconds (set by import)
    has_audio: Optional[bool] = None  # Pre-detected audio presence (set by import)
    end_on: Optional[
        str
    ] = None  # When video event ends: "next_slide" | "end" | "take" (None = marker-type default)


@dataclass
class VideoMetadata:
    """
    Metadata for a video source, typically from a .json file.

    This allows defining preprocessing steps separately from videos.json,
    enabling per-video preprocessing configuration.
    """

    source_file: str  # Original source video file
    preprocess: list[dict] = field(default_factory=list)  # Preprocessing filters
    output: Optional[
        dict
    ] = None  # Output config {"file": "...", "colorspace": "...", "alpha": "..."}


@dataclass
class SlideEvent:
    """A resolved slide event with timing information."""

    slide_id: str
    start_time: float
    end_time: float
    slide_def: SlideDefinition


@dataclass
class AudioDefinition:
    """Definition of an audio clip from audio.json."""

    file: str  # Audio filename (relative to audio.json location, or to shared_assets/media/audio/ if is_shared)
    volume: float = 1.0  # Volume multiplier (0.0-1.0)
    loop: bool = False  # If True, loop for entire duration from trigger point
    overlap: Optional[float] = None  # Crossfade overlap in seconds when looping
    ignore_pauses: bool = (
        False  # If True, audio continues playing during narration pauses
    )
    duration: Optional[float] = None  # Pre-probed duration in seconds (set by import)
    is_shared: bool = False  # If True, file is relative to shared_assets/media/audio/


@dataclass
class Citation:
    """A citation extracted from manuscript.txt [cite:...] markers."""

    reference: str  # The literal reference text after cite:
    marker_id: str  # The full marker (e.g., "cite:Smith et al...")
    timestamp: float = -1.0  # Aligned timestamp (-1 if not aligned)
    context: str = ""  # Text following the citation for alignment


@dataclass
class AudioEvent:
    """A resolved audio event with timing information."""

    audio_id: str
    start_time: float  # When to start playing (marker time - offset)
    audio_def: AudioDefinition


@dataclass
class VideoEvent:
    """A resolved video event with timing information."""

    video_id: str
    start_time: float
    end_time: float
    video_source: "VideoSource"
    cutout: "CutoutDefinition"
    cutout_name: str = ""  # resolved cutout name (e.g. "fullscreen"), for display
    layer: str = "above"  # "above" = on top of slides; "below" = behind slides


@dataclass
class CameraState:
    """State of the virtual camera at a point in time.

    The camera transforms the entire composed scene (background, slides, cutouts).
    This ensures all elements stay spatially synchronized when zooming/tilting.
    """

    zoom: float = 1.0  # 1.0 = 100%, 1.25 = 125%, etc.
    rotation: float = 0.0  # degrees, positive = clockwise
    pan_x: float = 0.0  # -1.0 to 1.0, percentage of frame width
    pan_y: float = 0.0  # -1.0 to 1.0, percentage of frame height
    focal_x: float = 0.5  # 0.0 to 1.0, zoom focal point X (0.5 = center)
    focal_y: float = 0.5  # 0.0 to 1.0, zoom focal point Y (0.5 = center)

    def __post_init__(self):
        # Clamp values to reasonable ranges
        self.zoom = max(0.5, min(3.0, self.zoom))
        self.rotation = max(-45.0, min(45.0, self.rotation))
        self.pan_x = max(-1.0, min(1.0, self.pan_x))
        self.pan_y = max(-1.0, min(1.0, self.pan_y))
        self.focal_x = max(0.0, min(1.0, self.focal_x))
        self.focal_y = max(0.0, min(1.0, self.focal_y))

    def is_default(self) -> bool:
        """Check if this is the default camera state (no transform)."""
        return (
            self.zoom == 1.0
            and self.rotation == 0.0
            and self.pan_x == 0.0
            and self.pan_y == 0.0
            and self.focal_x == 0.5
            and self.focal_y == 0.5
        )


@dataclass
class CameraEvent:
    """A camera state change at a specific time.

    Camera events can be instant (duration=0) or animated (duration>0).
    When animated, the camera smoothly transitions from its current state
    to the target state over the specified duration using the easing function.
    """

    time: float  # timestamp in seconds
    target_state: CameraState
    duration: float = 0.2  # transition duration (0 = instant snap)
    easing: str = "ease-out"  # linear, ease-in, ease-out, ease-in-out


# Camera effect presets - map marker names to camera states
# Effect strengths are intentionally subtle for professional look
CAMERA_PRESETS: dict[str, CameraState] = {
    # Zoom levels (halved for subtlety)
    "Zoom0": CameraState(zoom=1.0),
    "Zoom1": CameraState(zoom=1.05),
    "Zoom2": CameraState(zoom=1.125),
    "Zoom3": CameraState(zoom=1.25),
    # Tilt/rotation (halved)
    "TiltLeft": CameraState(rotation=-7.5),
    "TiltRight": CameraState(rotation=7.5),
    "NoTilt": CameraState(),  # Full reset to default state
    # Pan (halved)
    "PanLeft": CameraState(pan_x=-0.1),
    "PanRight": CameraState(pan_x=0.1),
    "PanUp": CameraState(pan_y=-0.075),
    "PanDown": CameraState(pan_y=0.075),
    "PanCenter": CameraState(pan_x=0.0, pan_y=0.0),
    # Reset all
    "Reset": CameraState(),
}


@dataclass
class NarrationPause:
    """A pause in the narration timeline for an interstitial video."""

    output_time: float  # When the pause starts in the OUTPUT timeline
    narration_time: float  # Where we are in the NARRATION source when pause starts
    duration: float  # How long the pause lasts
    video_id: str  # The video that plays during the pause


@dataclass
class OutroEvent:
    """A video that plays as part of the outro sequence (after narration ends)."""

    video_id: str
    start_time: float  # When this outro video starts (in output timeline)
    end_time: float  # When this outro video ends
    video_source: "VideoSource"
    cutout: Optional["CutoutDefinition"] = None  # None = fullscreen


@dataclass
class RenderPlan:
    """Complete plan for rendering the final video."""

    project_path: Path
    config: ProjectConfig
    slide_events: list[SlideEvent]
    total_duration: float
    slides: dict[str, SlideDefinition]
    videos: dict[str, VideoSource] = field(default_factory=dict)
    video_events: list[VideoEvent] = field(
        default_factory=list
    )  # Triggered video overlays
    narration_videos: list[tuple[str, VideoSource, CutoutDefinition]] = field(
        default_factory=list
    )  # (video_id, source, cutout)
    slides_dir: Path = None  # directory containing slide images
    videos_dir: Path = None  # directory containing videos.json and video files
    audio_events: list[AudioEvent] = field(default_factory=list)
    audio: dict[str, AudioDefinition] = field(default_factory=dict)
    audio_dir: Path = None  # directory containing audio.json and audio files
    camera_events: list[CameraEvent] = field(
        default_factory=list
    )  # Virtual camera keyframes
    # Partial rendering support
    time_offset: float = (
        0.0  # Offset subtracted from all timestamps (for partial render)
    )
    initial_camera_state: "CameraState" = (
        None  # Camera state at render start (for partial render)
    )
    input_seek_time: float = 0.0  # Seek position for input videos (for partial render)
    # Shared assets support
    shared_assets_dir: Path = None  # Directory containing shared assets (pexels, etc.)
    # Narration pause support
    narration_pauses: list[NarrationPause] = field(
        default_factory=list
    )  # Gaps in narration for interstitial videos
    # Outro sequence (plays after narration ends)
    outro_events: list["OutroEvent"] = field(
        default_factory=list
    )  # Videos that play after narration ends
    narration_end_time: float = 0.0  # When narration ends (before outro starts)
    # GnommoCache support
    cached_files: set = field(
        default_factory=set
    )  # Video IDs loaded from external cache (show 📁 indicator)
    output_path: Optional[
        Path
    ] = None  # Final output file path (set after plan is built)


# Slide layout configurations (hardcoded for POC)
SLIDE_LAYOUTS = {
    "fullscreen": {
        "x": 0,
        "y": 0,
        "width": 1920,
        "height": 1080,
    },
    "square": {
        "x": 560,  # centered horizontally: (1920 - 800) / 2
        "y": 140,  # positioned in upper area
        "width": 800,
        "height": 800,
    },
}
