from __future__ import annotations

import re
from dataclasses import dataclass

from timed_text import Cue, TimedTextDocument, TimingFidelity


TIMESTAMP_RE = re.compile(
    r"^(?P<hours>\d{1,3}):(?P<minutes>\d{2}):(?P<seconds>\d{2})[,.](?P<millis>\d{3})$"
)
TIMING_LINE_RE = re.compile(
    r"^\s*(?P<start>\d{1,3}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*"
    r"(?P<end>\d{1,3}:\d{2}:\d{2}[,.]\d{3})(?:\s+.*)?$"
)


@dataclass(frozen=True)
class SrtDiagnostic:
    severity: str
    code: str
    message: str
    block: int | None = None


class SrtParseError(ValueError):
    def __init__(self, diagnostics: list[SrtDiagnostic]) -> None:
        self.diagnostics = diagnostics
        super().__init__("; ".join(item.message for item in diagnostics))


def parse_timestamp(value: str) -> int:
    match = TIMESTAMP_RE.fullmatch(value.strip())
    if not match:
        raise ValueError(f"Invalid SRT timestamp: {value!r}")
    hours = int(match.group("hours"))
    minutes = int(match.group("minutes"))
    seconds = int(match.group("seconds"))
    millis = int(match.group("millis"))
    if minutes > 59 or seconds > 59:
        raise ValueError(f"Invalid SRT timestamp: {value!r}")
    return ((hours * 60 + minutes) * 60 + seconds) * 1000 + millis


def format_timestamp(milliseconds: int) -> str:
    if milliseconds < 0:
        raise ValueError("SRT timestamp must be non-negative.")
    hours, remainder = divmod(milliseconds, 3_600_000)
    minutes, remainder = divmod(remainder, 60_000)
    seconds, millis = divmod(remainder, 1000)
    return f"{hours:02d}:{minutes:02d}:{seconds:02d},{millis:03d}"


def normalize_cue_text(value: str) -> str:
    lines = value.replace("\r\n", "\n").replace("\r", "\n").split("\n")
    while lines and not lines[0].strip():
        lines.pop(0)
    while lines and not lines[-1].strip():
        lines.pop()
    return "\n".join(line.rstrip() for line in lines)


def parse_srt(
    text: str,
    *,
    language: str = "",
    source_name: str = "",
    strict: bool = True,
) -> tuple[TimedTextDocument, list[SrtDiagnostic]]:
    normalized = text.lstrip("\ufeff").replace("\r\n", "\n").replace("\r", "\n").strip()
    diagnostics: list[SrtDiagnostic] = []
    if not normalized:
        document = TimedTextDocument(
            language=language,
            source_kind="srt",
            source_name=source_name,
            timing_fidelity=TimingFidelity.CUE_ALIGNED_ONLY,
            cues=[],
        )
        return document, diagnostics

    blocks = re.split(r"\n[ \t]*\n+", normalized)
    cues: list[Cue] = []
    fatal: list[SrtDiagnostic] = []
    for block_number, block in enumerate(blocks, start=1):
        lines = block.split("\n")
        timing_position = next(
            (index for index, line in enumerate(lines[:2]) if TIMING_LINE_RE.match(line)), None
        )
        if timing_position is None:
            fatal.append(
                SrtDiagnostic("ERROR", "MISSING_TIMING_LINE", "Cue has no valid timing line.", block_number)
            )
            continue
        if timing_position == 0:
            diagnostics.append(
                SrtDiagnostic(
                    "WARNING",
                    "MISSING_INDEX",
                    "Cue index is missing; canonical output will add it.",
                    block_number,
                )
            )
        else:
            try:
                declared_index = int(lines[0].strip())
            except ValueError:
                fatal.append(
                    SrtDiagnostic("ERROR", "INVALID_INDEX", "Cue index is not an integer.", block_number)
                )
                continue
            if declared_index != len(cues) + 1:
                diagnostics.append(
                    SrtDiagnostic(
                        "WARNING",
                        "NON_SEQUENTIAL_INDEX",
                        "Cue index is not sequential; canonical output will renumber it.",
                        block_number,
                    )
                )
        timing_match = TIMING_LINE_RE.match(lines[timing_position])
        assert timing_match is not None
        try:
            start_ms = parse_timestamp(timing_match.group("start"))
            end_ms = parse_timestamp(timing_match.group("end"))
        except ValueError as exc:
            fatal.append(SrtDiagnostic("ERROR", "INVALID_TIMESTAMP", str(exc), block_number))
            continue
        cue_text = normalize_cue_text("\n".join(lines[timing_position + 1 :]))
        if end_ms <= start_ms:
            fatal.append(
                SrtDiagnostic(
                    "ERROR", "NON_POSITIVE_DURATION", "Cue end must be greater than start.", block_number
                )
            )
            continue
        if not cue_text:
            fatal.append(SrtDiagnostic("ERROR", "EMPTY_TEXT", "Cue text is empty.", block_number))
            continue
        cues.append(
            Cue(
                id=f"cue_{len(cues) + 1:06d}",
                srt_index=len(cues) + 1,
                start_ms=start_ms,
                end_ms=end_ms,
                text=cue_text,
            )
        )

    if fatal and strict:
        raise SrtParseError(fatal + diagnostics)
    diagnostics = fatal + diagnostics
    document = TimedTextDocument(
        language=language,
        source_kind="srt",
        source_name=source_name,
        timing_fidelity=TimingFidelity.CUE_ALIGNED_ONLY,
        cues=cues,
    )
    document.validate()
    return document, diagnostics


def serialize_srt(document: TimedTextDocument) -> str:
    document.validate()
    blocks: list[str] = []
    for index, cue in enumerate(document.cues, start=1):
        blocks.append(
            "\n".join(
                [
                    str(index),
                    f"{format_timestamp(cue.start_ms)} --> {format_timestamp(cue.end_ms)}",
                    normalize_cue_text(cue.text),
                ]
            )
        )
    return "\n\n".join(blocks) + ("\n" if blocks else "")
