"""
LiveTranscriptionService - streaming dictation (local path).

While the user speaks, the growing audio buffer is re-transcribed every
~POLL_INTERVAL seconds. Words that stay stable across two consecutive
hypotheses (LocalAgreement-2, ufal/whisper_streaming) are "committed" and
emitted as a delta so the app can type them at the cursor in real time.
Committed words are never retracted.

The commit state machine lives in ``local_agreement.LocalAgreementCommitter``
and is shared with the server's /v1/realtime session. This service owns the
local specifics: the mic audio source (snapshot + new-audio throttle), running
inference off the event loop, and emitting committed deltas.

No AI enhancement runs in live mode (enhancement rewrites the whole text,
which is incompatible with streaming).
"""
from __future__ import annotations

import asyncio
import functools
from typing import TYPE_CHECKING

from loguru import logger

from .base_service import BaseService
from .local_agreement import (  # re-exported for callers/tests
    FORCE_KEEP_SECONDS,
    HARD_CAP_SECONDS,
    MIN_AUDIO_TO_START_S,
    MIN_NEW_AUDIO_S,
    POLL_INTERVAL,
    REANCHOR_SECONDS,
    SAMPLE_RATE,
    LocalAgreementCommitter,
    commit_agreement,
    trim_trailing_silence,
)

if TYPE_CHECKING:
    from bridge.signals import AppSignals

    from .audio_service import AudioService
    from .transcription_service import TranscriptionService

__all__ = [
    "LiveTranscriptionService", "commit_agreement", "SAMPLE_RATE",
    "HARD_CAP_SECONDS", "FORCE_KEEP_SECONDS", "REANCHOR_SECONDS",
    "MIN_NEW_AUDIO_S", "MIN_AUDIO_TO_START_S", "POLL_INTERVAL",
]


class LiveTranscriptionService(BaseService):
    def __init__(
        self,
        signals: AppSignals,
        audio_service: AudioService,
        transcription_service: TranscriptionService,
    ) -> None:
        self._signals = signals
        self._audio = audio_service
        self._transcription = transcription_service
        self._task: asyncio.Task | None = None
        self._running = False
        self._committer = LocalAgreementCommitter()

    # ── Backward-compatible view onto the committer state (tests + internals) ──
    @property
    def _language(self) -> str | None:
        return self._committer.language

    @_language.setter
    def _language(self, value: str | None) -> None:
        self._committer.language = value

    @property
    def _committed(self) -> list[dict]:
        return self._committer.committed

    @_committed.setter
    def _committed(self, value: list[dict]) -> None:
        self._committer.committed = value

    @property
    def _anchor_samples(self) -> int:
        return self._committer.anchor_samples

    @_anchor_samples.setter
    def _anchor_samples(self, value: int) -> None:
        self._committer.anchor_samples = value

    @property
    def _prev_norm(self) -> list[str]:
        return self._committer.prev_norm

    @_prev_norm.setter
    def _prev_norm(self, value: list[str]) -> None:
        self._committer.prev_norm = value

    @property
    def _seam_norm(self) -> str:
        return self._committer.seam_norm

    @_seam_norm.setter
    def _seam_norm(self, value: str) -> None:
        self._committer.seam_norm = value

    def _window(self, full, base):
        return self._committer.window(full, base)

    def _maybe_reanchor(self, window_samples: int) -> None:
        self._committer.maybe_reanchor(window_samples)

    def _reset(self) -> None:
        self._committer.reset()

    async def start(self) -> None:
        logger.info("LiveTranscriptionService ready")

    async def stop(self) -> None:
        await self.stop_stream()

    @property
    def is_ready(self) -> bool:
        return True

    @property
    def running(self) -> bool:
        return self._running

    async def start_stream(self, language: str | None) -> None:
        """Begin the streaming loop (call right after recording starts)."""
        if self._running:
            return
        if not self._transcription.is_ready:
            logger.warning("Live stream requested but transcription not ready")
            return
        self._committer.reset()
        self._committer.language = language
        self._running = True
        self._task = asyncio.get_running_loop().create_task(self._loop())
        logger.info("Live transcription stream started (lang={})", language or "auto")

    async def stop_stream(self) -> str:
        """Stop the loop, do a final transcription, commit the remaining tail.
        Returns the full committed text of the session."""
        if not self._running:
            return ""
        self._running = False
        if self._task is not None:
            try:
                await self._task
            except asyncio.CancelledError:
                pass
            self._task = None
        # Final pass: commit everything still pending in the current window.
        try:
            await self._transcribe_once(final=True)
        except Exception:
            logger.exception("Live final transcription failed")
        return ""

    async def _loop(self) -> None:
        # small initial delay so there is audio to work with
        await asyncio.sleep(POLL_INTERVAL)
        while self._running:
            try:
                await self._transcribe_once(final=False)
            except Exception:
                logger.exception("Live transcription iteration failed")
            await asyncio.sleep(POLL_INTERVAL)

    async def _transcribe_once(self, *, final: bool) -> None:
        loop = asyncio.get_running_loop()
        # Cheap throttle FIRST (no copy/resample): skip until enough NEW audio
        # arrived, so a paused user does not trigger an O(n) snapshot per tick.
        if not final:
            avail = self._audio.captured_samples_16k()
            if avail - self._committer.last_infer_abs < int(MIN_NEW_AUDIO_S * SAMPLE_RATE):
                return
        # The concatenate+resample is O(session length); do it off the event
        # loop so long sessions don't degrade UI/hotkey latency.
        full, base = await loop.run_in_executor(None, self._audio.snapshot_buffer)
        # Window selection + inference + commit run off the loop too (the
        # transcribe call blocks); the committer returns the delta to emit.
        delta = await loop.run_in_executor(
            None,
            functools.partial(
                self._committer.process,
                full, base, self._transcription.transcribe_window_sync,
                final=final,
            ),
        )
        if delta:
            self._signals.live_text_committed.emit(delta)
