"""Deterministic renderer-owned subtitle cue planning."""

from __future__ import annotations

import re
from dataclasses import dataclass, replace

from autoshorts.ideas.candidate import ValidationResult


_WORD_RE = re.compile(r"[A-Za-z0-9]+(?:['-][A-Za-z0-9]+)?")


@dataclass(frozen=True)
class SubtitleStyle:
    name: str
    primary_color: str
    accent_color: str
    stroke_color: str
    shadow_color: str
    placement: str
    max_lines: int
    min_words_per_cue: int
    max_words_per_cue: int
    safe_zone_bottom_percent: int
    font_weight: str
    font_size: int = 86
    margin_v: int = 260

    @classmethod
    def hormozi_inspired(cls) -> "SubtitleStyle":
        """Return the TrueTrace kinetic-caption baseline: inspired mechanics, original brand skin."""

        return cls(
            name="truetrace_hormozi_inspired",
            primary_color="#FFFFFF",
            accent_color="#22D3EE",
            stroke_color="#050816",
            shadow_color="#000000",
            placement="lower_middle_above_ui_safe_zone",
            max_lines=2,
            min_words_per_cue=2,
            max_words_per_cue=5,
            safe_zone_bottom_percent=14,
            font_weight="800",
        )

    @classmethod
    def final_motion_compact(cls) -> "SubtitleStyle":
        """Return a compact final-motion style that leaves mechanism diagrams breathing room."""

        return cls(
            name="truetrace_final_motion_compact",
            primary_color="#FFFFFF",
            accent_color="#22D3EE",
            stroke_color="#050816",
            shadow_color="#000000",
            placement="lower_middle_above_ui_safe_zone",
            max_lines=2,
            min_words_per_cue=1,
            max_words_per_cue=4,
            safe_zone_bottom_percent=17,
            font_weight="800",
            font_size=72,
            margin_v=320,
        )


@dataclass(frozen=True)
class SubtitleCue:
    text: str
    start_ms: int
    end_ms: int
    active_word: str
    renderer_owned: bool = True
    max_lines: int = 2
    emphasis_color: str = "#22D3EE"


@dataclass(frozen=True)
class SubtitleTrack:
    cues: tuple[SubtitleCue, ...]
    style: SubtitleStyle
    renderer_owned: bool = True
    side_effects: tuple[str, ...] = ()
    external_calls: tuple[str, ...] = ()

    def with_cues(self, cues: tuple[SubtitleCue, ...]) -> "SubtitleTrack":
        return replace(self, cues=cues)


def _words(text: str) -> list[str]:
    return _WORD_RE.findall(text)


def _chunk_words(words: list[str], *, min_words: int, max_words: int) -> list[list[str]]:
    chunks: list[list[str]] = []
    index = 0
    while index < len(words):
        remaining = len(words) - index
        take = min(max_words, remaining)
        if 0 < remaining < min_words and chunks:
            chunks[-1].extend(words[index:])
            break
        if remaining == min_words + 1:
            take = min_words
        chunks.append(words[index : index + take])
        index += take
    return chunks


def _semantic_word_chunks(text: str, *, min_words: int, max_words: int) -> list[list[str]]:
    chunks: list[list[str]] = []
    for segment in re.split(r"[.,:;!?]+", text):
        words = _words(segment)
        if not words:
            continue
        chunks.extend(_chunk_words(words, min_words=min_words, max_words=max_words))
    return chunks


def _active_word(chunk: list[str]) -> str:
    # Prefer the longest meaningful word so the highlighted token carries semantic weight.
    return max((word.upper() for word in chunk), key=lambda word: (len(word), word))


def _build_subtitle_track(
    text: str,
    *,
    total_duration_seconds: int,
    style: SubtitleStyle,
    preserve_phrase_boundaries: bool = False,
) -> SubtitleTrack:
    if not text.strip():
        raise ValueError("subtitle text is required")
    if total_duration_seconds <= 0:
        raise ValueError("total_duration_seconds must be positive")

    words = _words(text)
    if len(words) < style.min_words_per_cue:
        raise ValueError("subtitle text must contain at least two words")

    if preserve_phrase_boundaries:
        chunks = _semantic_word_chunks(text, min_words=style.min_words_per_cue, max_words=style.max_words_per_cue)
    else:
        chunks = _chunk_words(words, min_words=style.min_words_per_cue, max_words=style.max_words_per_cue)
    total_ms = total_duration_seconds * 1000
    cues: list[SubtitleCue] = []
    for index, chunk in enumerate(chunks):
        start_ms = round(index * total_ms / len(chunks))
        end_ms = round((index + 1) * total_ms / len(chunks))
        cues.append(
            SubtitleCue(
                text=" ".join(word.upper() for word in chunk),
                start_ms=start_ms,
                end_ms=end_ms,
                active_word=_active_word(chunk),
                renderer_owned=True,
                max_lines=style.max_lines,
                emphasis_color=style.accent_color,
            )
        )

    track = SubtitleTrack(cues=tuple(cues), style=style)
    validation = validate_subtitle_track(track)
    if not validation.is_valid:
        raise ValueError("; ".join(validation.errors))
    return track


def build_hormozi_subtitle_track(text: str, *, total_duration_seconds: int) -> SubtitleTrack:
    """Create continuous 2..5-word renderer-owned subtitle cues for final burn-in."""

    return _build_subtitle_track(
        text,
        total_duration_seconds=total_duration_seconds,
        style=SubtitleStyle.hormozi_inspired(),
    )


def build_final_motion_subtitle_track(text: str, *, total_duration_seconds: int) -> SubtitleTrack:
    """Create compact 1..3-word cues for final-near mechanism videos."""

    return _build_subtitle_track(
        text,
        total_duration_seconds=total_duration_seconds,
        style=SubtitleStyle.final_motion_compact(),
        preserve_phrase_boundaries=True,
    )


def validate_subtitle_track(track: SubtitleTrack) -> ValidationResult:
    errors: list[str] = []
    if not track.renderer_owned:
        errors.append("subtitle track must be renderer-owned")
    if track.side_effects:
        errors.append("subtitle track must not have side effects")
    if track.external_calls:
        errors.append("subtitle track must not have external calls")
    if not track.cues:
        errors.append("at least one subtitle cue is required")

    previous_end: int | None = None
    for index, cue in enumerate(track.cues, start=1):
        cue_words = cue.text.split()
        if not cue.renderer_owned:
            errors.append(f"subtitle cue {index} must be renderer-owned")
        if not (track.style.min_words_per_cue <= len(cue_words) <= track.style.max_words_per_cue):
            errors.append(f"subtitle cue {index} must contain 2..5 words")
        if cue.end_ms <= cue.start_ms:
            errors.append(f"subtitle cue {index} end_ms must be greater than start_ms")
        if previous_end is None:
            if cue.start_ms != 0:
                errors.append("first subtitle cue must start at 0ms")
        elif cue.start_ms != previous_end:
            errors.append(f"subtitle cue {index} must start when the previous cue ends")
        if not cue.active_word.strip():
            errors.append(f"subtitle cue {index} active_word is required")
        elif cue.active_word not in {word.upper() for word in cue_words}:
            errors.append(f"subtitle cue {index} active_word must appear in cue text")
        if cue.max_lines > track.style.max_lines:
            errors.append(f"subtitle cue {index} exceeds style max lines")
        previous_end = cue.end_ms

    return ValidationResult(is_valid=not errors, errors=tuple(errors))
