"""Реконструкция фраз из сегментов Whisper по алгоритму ТЗ §1.3.

Определение фразы (ТЗ §1.3): фрагмент монолога одного участника между
монологами других участников, либо между `t_start` сеанса и первым
монологом, либо между последним монологом и `t_end`.

Алгоритм:
1. Сегменты всех треков (участников) сливаются в единый таймлайн с учётом
   смещения каждого трека (`track_offsets`) и сортируются по `start`.
2. Идём по таймлайну и группируем подряд идущие сегменты одного и того же
   участника в одну фразу; смена участника — граница фразы.
3. Короткая вставка другого участника короче `INTERJECTION_THRESHOLD_S`
   (например, «угу») не прерывает текущую фразу, но сама сохраняется
   отдельной фразой.
4. Тишина (пауза) между сегментами одного участника без чужой речи фразу
   не рвёт.
5. Перекрытия речи двух участников длиннее порога — обе фразы сохраняются
   с пересекающимися интервалами (без «доминирующего спикера»).

Функция `build_phrases` — чистая: работает в секундах, относительных
`t_start` сеанса; конвертацию в абсолютное время UTC делает вызывающий код
(оркестратор пайплайна).
"""

import uuid
from dataclasses import dataclass, field

from core.plugins.transcriber import Segment

INTERJECTION_THRESHOLD_S: float = 1.5
"""Порог длительности (в секундах) короткой вставки другого спикера,
которая не прерывает текущую фразу, но фиксируется как отдельная фраза."""


@dataclass(frozen=True, slots=True)
class SpeakerSegment:
    """Сегмент речи одного участника на объединённом таймлайне сеанса."""

    participant_id: uuid.UUID
    start: float  # секунды от t_start сеанса
    end: float
    text: str


@dataclass(frozen=True, slots=True)
class PhraseDraft:
    """Черновик реконструированной фразы — без привязки к сеансу/БД."""

    participant_id: uuid.UUID
    start: float
    end: float
    text: str


@dataclass(slots=True)
class _OpenPhrase:
    """Мутируемое состояние фразы, которая ещё собирается в процессе обхода таймлайна."""

    participant_id: uuid.UUID
    start: float
    end: float
    texts: list[str] = field(default_factory=list)

    def to_draft(self) -> PhraseDraft:
        """Зафиксировать накопленное состояние как неизменяемый `PhraseDraft`."""
        return PhraseDraft(
            participant_id=self.participant_id,
            start=self.start,
            end=self.end,
            text=" ".join(text for text in self.texts if text),
        )


def _merge_timeline(
    segments_by_participant: dict[uuid.UUID, list[Segment]],
    track_offsets: dict[uuid.UUID, float],
) -> list[SpeakerSegment]:
    """Слить сегменты всех участников в единый таймлайн, отсортированный по `start`.

    Смещение (`track_offsets`) — время старта записи трека участника
    относительно `t_start` сеанса; при отсутствии участника в словаре
    смещений используется 0.0.
    """
    timeline: list[SpeakerSegment] = []
    for participant_id, segments in segments_by_participant.items():
        offset = track_offsets.get(participant_id, 0.0)
        for segment in segments:
            timeline.append(
                SpeakerSegment(
                    participant_id=participant_id,
                    start=segment.start + offset,
                    end=segment.end + offset,
                    text=segment.text,
                )
            )
    timeline.sort(key=lambda s: (s.start, s.end))
    return timeline


def build_phrases(
    segments_by_participant: dict[uuid.UUID, list[Segment]],
    track_offsets: dict[uuid.UUID, float],
) -> list[PhraseDraft]:
    """Реконструировать фразы по объединённому таймлайну сегментов участников.

    Аргументы:
        segments_by_participant: сегменты Whisper каждого участника,
            относительные началу его собственного трека.
        track_offsets: смещение (в секундах от `t_start` сеанса) начала
            записи каждого трека.

    Возвращает список `PhraseDraft`, отсортированный по `start`.
    """
    timeline = _merge_timeline(segments_by_participant, track_offsets)
    if not timeline:
        return []

    finished: list[PhraseDraft] = []
    current: _OpenPhrase | None = None

    for segment in timeline:
        if current is None:
            current = _OpenPhrase(
                participant_id=segment.participant_id,
                start=segment.start,
                end=segment.end,
                texts=[segment.text],
            )
            continue

        if segment.participant_id == current.participant_id:
            # тот же спикер: продолжаем фразу, тишина между сегментами её не рвёт
            current.end = max(current.end, segment.end)
            current.texts.append(segment.text)
            continue

        duration = segment.end - segment.start
        if duration < INTERJECTION_THRESHOLD_S:
            # короткая вставка чужого спикера: не прерывает текущую фразу,
            # но фиксируется как отдельная фраза
            finished.append(
                PhraseDraft(
                    participant_id=segment.participant_id,
                    start=segment.start,
                    end=segment.end,
                    text=segment.text,
                )
            )
            continue

        # реальная смена спикера (или длинное перекрытие) — граница фразы
        finished.append(current.to_draft())
        current = _OpenPhrase(
            participant_id=segment.participant_id,
            start=segment.start,
            end=segment.end,
            texts=[segment.text],
        )

    if current is not None:
        finished.append(current.to_draft())

    finished.sort(key=lambda p: (p.start, p.end))
    return finished
