from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Any


class TimingFidelity(str, Enum):
    WORD_ALIGNED_EXACT = "WORD_ALIGNED_EXACT"
    WORD_ALIGNED_ESTIMATED = "WORD_ALIGNED_ESTIMATED"
    CUE_ALIGNED_ONLY = "CUE_ALIGNED_ONLY"
    ESTIMATED_WITHIN_GROUP = "ESTIMATED_WITHIN_GROUP"


class WordTimingSource(str, Enum):
    TRANSCRIPT_EXACT = "TRANSCRIPT_EXACT"
    TRANSCRIPT_NEAR = "TRANSCRIPT_NEAR"
    INTERPOLATED_BETWEEN_ANCHORS = "INTERPOLATED_BETWEEN_ANCHORS"
    EDGE_ESTIMATE = "EDGE_ESTIMATE"
    SHARED_ANCHOR_WINDOW = "SHARED_ANCHOR_WINDOW"


TIMING_FIDELITY_RANK = {
    TimingFidelity.WORD_ALIGNED_EXACT: 0,
    TimingFidelity.WORD_ALIGNED_ESTIMATED: 1,
    TimingFidelity.CUE_ALIGNED_ONLY: 2,
    TimingFidelity.ESTIMATED_WITHIN_GROUP: 3,
}

WORD_TIMING_SOURCE_RANK = {
    WordTimingSource.TRANSCRIPT_EXACT: 0,
    WordTimingSource.TRANSCRIPT_NEAR: 1,
    WordTimingSource.INTERPOLATED_BETWEEN_ANCHORS: 2,
    WordTimingSource.EDGE_ESTIMATE: 3,
    WordTimingSource.SHARED_ANCHOR_WINDOW: 4,
}


def worst_timing_fidelity(
    values: list[TimingFidelity] | tuple[TimingFidelity, ...],
) -> TimingFidelity:
    if not values:
        raise ValueError("At least one timing fidelity is required.")
    return max(values, key=TIMING_FIDELITY_RANK.__getitem__)


def worst_word_timing_source(
    values: list[WordTimingSource] | tuple[WordTimingSource, ...],
) -> WordTimingSource:
    if not values:
        raise ValueError("At least one word timing source is required.")
    return max(values, key=WORD_TIMING_SOURCE_RANK.__getitem__)


@dataclass(frozen=True)
class TimedWord:
    text: str
    start_ms: int
    end_ms: int
    timing_source: WordTimingSource = WordTimingSource.TRANSCRIPT_EXACT

    def validate(self, cue_start_ms: int, cue_end_ms: int) -> None:
        if not self.text.strip():
            raise ValueError("Timed word text must not be empty.")
        if self.start_ms < cue_start_ms or self.end_ms > cue_end_ms:
            raise ValueError("Timed word must be contained by its cue.")
        if self.end_ms <= self.start_ms:
            raise ValueError("Timed word end must be greater than its start.")


@dataclass
class Cue:
    id: str
    srt_index: int
    start_ms: int
    end_ms: int
    text: str
    speaker: str = ""
    source_track: str = ""
    source_item: str = ""
    words: list[TimedWord] = field(default_factory=list)
    metadata: dict[str, Any] = field(default_factory=dict)

    def validate(self, fidelity: TimingFidelity) -> None:
        if not self.id:
            raise ValueError("Cue id must not be empty.")
        if self.srt_index < 1:
            raise ValueError("Cue SRT index must be at least 1.")
        if self.start_ms < 0:
            raise ValueError("Cue start must be non-negative.")
        if self.end_ms <= self.start_ms:
            raise ValueError("Cue end must be greater than cue start.")
        if not self.text.strip():
            raise ValueError("Cue text must not be empty.")
        previous_word: TimedWord | None = None
        for word in self.words:
            word.validate(self.start_ms, self.end_ms)
            if previous_word is not None:
                if word.start_ms < previous_word.start_ms:
                    raise ValueError("Timed words must be monotonic by start time.")
                if word.start_ms < previous_word.end_ms and not (
                    fidelity == TimingFidelity.WORD_ALIGNED_ESTIMATED
                    and previous_word.timing_source
                    == WordTimingSource.SHARED_ANCHOR_WINDOW
                    and word.timing_source == WordTimingSource.SHARED_ANCHOR_WINDOW
                ):
                    raise ValueError(
                        "Timed words must not overlap outside a shared anchor-window run."
                    )
            previous_word = word
        if self.words and fidelity not in {
            TimingFidelity.WORD_ALIGNED_EXACT,
            TimingFidelity.WORD_ALIGNED_ESTIMATED,
        }:
            raise ValueError(
                "Timed words require word-aligned exact or estimated timing fidelity."
            )
        if fidelity == TimingFidelity.WORD_ALIGNED_EXACT and any(
            word.timing_source != WordTimingSource.TRANSCRIPT_EXACT
            for word in self.words
        ):
            raise ValueError(
                "WORD_ALIGNED_EXACT words require TRANSCRIPT_EXACT timing sources."
            )


@dataclass
class TimedTextDocument:
    language: str
    source_kind: str
    source_name: str
    timing_fidelity: TimingFidelity
    cues: list[Cue]
    version: int = 1
    metadata: dict[str, Any] = field(default_factory=dict)

    def validate(self, *, allow_overlaps: bool = True) -> None:
        if self.version != 1:
            raise ValueError(f"Unsupported timed-text document version: {self.version}")
        ids: set[str] = set()
        previous_start = -1
        previous_end = -1
        for position, cue in enumerate(self.cues, start=1):
            cue.validate(self.timing_fidelity)
            if cue.id in ids:
                raise ValueError(f"Duplicate cue id: {cue.id}")
            ids.add(cue.id)
            if cue.srt_index != position:
                raise ValueError("Cue srtIndex values must be sequential and 1-based.")
            if cue.start_ms < previous_start:
                raise ValueError("Cue source ordering must be chronological.")
            if not allow_overlaps and cue.start_ms < previous_end:
                raise ValueError("Cues must not overlap.")
            previous_start = cue.start_ms
            previous_end = max(previous_end, cue.end_ms)
        if self.timing_fidelity == TimingFidelity.WORD_ALIGNED_ESTIMATED:
            words = [word for cue in self.cues for word in cue.words]
            if not words:
                raise ValueError(
                    "WORD_ALIGNED_ESTIMATED requires stored word timing."
                )
            if all(
                word.timing_source == WordTimingSource.TRANSCRIPT_EXACT
                for word in words
            ):
                raise ValueError(
                    "WORD_ALIGNED_ESTIMATED requires at least one estimated timing source."
                )

    def reindex(self) -> None:
        for index, cue in enumerate(self.cues, start=1):
            cue.srt_index = index
            cue.id = f"cue_{index:06d}"


def document_to_dict(document: TimedTextDocument) -> dict[str, Any]:
    return {
        "version": document.version,
        "language": document.language,
        "source": {
            "kind": document.source_kind,
            "name": document.source_name,
            "timingFidelity": document.timing_fidelity.value,
        },
        "cues": [
            {
                "id": cue.id,
                "srtIndex": cue.srt_index,
                "startMs": cue.start_ms,
                "endMs": cue.end_ms,
                "text": cue.text,
                "speaker": cue.speaker,
                "sourceTrack": cue.source_track,
                "sourceItem": cue.source_item,
                "words": [
                    {
                        "text": word.text,
                        "startMs": word.start_ms,
                        "endMs": word.end_ms,
                        "timingSource": word.timing_source.value,
                    }
                    for word in cue.words
                ],
                **({"metadata": cue.metadata} if cue.metadata else {}),
            }
            for cue in document.cues
        ],
        **({"metadata": document.metadata} if document.metadata else {}),
    }
