"""Deepgram streaming transcriber (primary) with local Whisper fallback.

CRITICAL: Must be imported only after daemonize() — same as capture.py.
"""

from __future__ import annotations

import logging
import threading
import time
from typing import TYPE_CHECKING, Optional

import numpy as np

if TYPE_CHECKING:
    from meeting_helper.audio.capture import CombinedAudioCapture
    from meeting_helper.buffer import SentenceBuffer

logger = logging.getLogger(__name__)

SAMPLE_RATE = 16000


class DeepgramTranscriber:
    """Streams audio to Deepgram and appends final transcripts to the SentenceBuffer."""

    def __init__(
        self,
        audio_capture: CombinedAudioCapture,
        transcript_buffer: SentenceBuffer,
        api_key: str,
        language: str = "en",
    ) -> None:
        self._capture = audio_capture
        self._buffer = transcript_buffer
        self._language = language
        self._api_key = api_key
        self._stop_event = threading.Event()
        self._thread: Optional[threading.Thread] = None
        self._connection = None
        self._last_interim: Optional[str] = None
        self._last_transcript_time: float = 0.0
        self._reconnect_attempts: int = 0

    def _on_message(self, sender, result=None, **kwargs):
        try:
            if result is None:
                return
            self._last_transcript_time = time.time()
            sentence = result.channel.alternatives[0].transcript
            if not sentence:
                return

            from meeting_helper.state import state
            loop = state.asyncio_loop

            if result.is_final:
                logger.info("Deepgram final: %s", sentence[:120])
                self._last_interim = None
                self._buffer.append_final(sentence)
                if loop is not None:
                    import asyncio
                    asyncio.run_coroutine_threadsafe(
                        self._broadcast_transcript(sentence, interim=False), loop
                    )
            else:
                self._last_interim = sentence
                self._buffer.set_interim(sentence)
                if loop is not None:
                    import asyncio
                    asyncio.run_coroutine_threadsafe(
                        self._broadcast_transcript(sentence, interim=True), loop
                    )
        except Exception as exc:
            logger.warning("Deepgram message parse error: %s", exc)

    @staticmethod
    async def _broadcast_transcript(sentence: str, interim: bool = False) -> None:
        try:
            from meeting_helper.server.websocket import broadcast
            await broadcast({"type": "transcript", "text": sentence, "interim": interim})
        except Exception:
            pass

    def _on_utterance_end(self, sender, utterance_end=None, **kwargs):
        try:
            last = self._last_interim
            if last:
                logger.info("Deepgram utterance_end flush: %s", last[:80])
                self._buffer.append_final(last)
                from meeting_helper.state import state
                loop = state.asyncio_loop
                if loop is not None:
                    import asyncio
                    asyncio.run_coroutine_threadsafe(
                        self._broadcast_transcript(last, interim=False), loop
                    )
                self._last_interim = None
        except Exception as exc:
            logger.warning("Deepgram utterance_end error: %s", exc)

    def _on_speech_started(self, sender, speech_started=None, **kwargs):
        from meeting_helper.state import state
        loop = state.asyncio_loop
        if loop is not None:
            import asyncio
            asyncio.run_coroutine_threadsafe(self._broadcast_speech_started(), loop)

    @staticmethod
    async def _broadcast_speech_started() -> None:
        try:
            from meeting_helper.server.websocket import broadcast
            await broadcast({"type": "speech_started"})
        except Exception:
            pass

    def _on_error(self, sender, error=None, **kwargs):
        logger.warning("Deepgram error: %s", error)
        if not self._stop_event.is_set():
            threading.Thread(target=self._reconnect, daemon=True).start()

    def _on_close(self, sender, close=None, **kwargs):
        logger.warning("Deepgram connection closed — reconnecting...")
        if not self._stop_event.is_set():
            threading.Thread(target=self._reconnect, daemon=True).start()

    def _reconnect(self) -> None:
        if self._stop_event.is_set():
            return

        self._reconnect_attempts += 1
        delay = min(2 ** self._reconnect_attempts, 30)
        logger.info("Deepgram reconnecting in %ds (attempt %d)...", delay, self._reconnect_attempts)
        time.sleep(delay)

        if self._stop_event.is_set():
            return

        try:
            if self._connection:
                try:
                    self._connection.finish()
                except Exception:
                    pass
        except Exception:
            pass
        self._connection = None

        if self._start_connection():
            self._reconnect_attempts = 0
            self._last_transcript_time = time.time()
            logger.info("Deepgram reconnected successfully")
            self._thread = threading.Thread(target=self._feed_loop, daemon=True)
            self._thread.start()
        elif self._reconnect_attempts < 10:
            threading.Thread(target=self._reconnect, daemon=True).start()
        else:
            logger.error("Deepgram reconnect failed after %d attempts", self._reconnect_attempts)

    def _feed_loop(self):
        last_keepalive = time.time()
        self._last_transcript_time = time.time()
        _WATCHDOG_TIMEOUT = 60

        while not self._stop_event.is_set():
            chunk = self._capture.get_transcriber_audio(timeout=0.01)
            if self._connection is None:
                time.sleep(0.1)
                continue
            if chunk is None:
                if time.time() - last_keepalive > 8:
                    try:
                        self._connection.keep_alive()
                        last_keepalive = time.time()
                    except Exception:
                        pass
                continue
            try:
                pcm = (chunk * 32767).astype(np.int16).tobytes()
                self._connection.send(pcm)
                last_keepalive = time.time()
            except Exception as exc:
                logger.warning("Deepgram send error: %s", exc)
                if not self._stop_event.is_set():
                    threading.Thread(target=self._reconnect, daemon=True).start()
                break

            now = time.time()
            silence_duration = now - self._last_transcript_time
            if silence_duration > _WATCHDOG_TIMEOUT:
                logger.warning("Deepgram watchdog: no transcript for %ds", int(silence_duration))
                threading.Thread(target=self._reconnect, daemon=True).start()
                break

    def _start_connection(self) -> bool:
        try:
            import ssl
            import certifi
            import os
            os.environ.setdefault("SSL_CERT_FILE", certifi.where())
            os.environ.setdefault("REQUESTS_CA_BUNDLE", certifi.where())

            from deepgram import DeepgramClient, LiveTranscriptionEvents, LiveOptions

            dg = DeepgramClient(api_key=self._api_key)
            conn = dg.listen.live.v("1")
            conn.on(LiveTranscriptionEvents.Transcript, self._on_message)
            conn.on(LiveTranscriptionEvents.Error, self._on_error)
            try:
                conn.on(LiveTranscriptionEvents.Close, self._on_close)
                conn.on(LiveTranscriptionEvents.SpeechStarted, self._on_speech_started)
                conn.on(LiveTranscriptionEvents.UtteranceEnd, self._on_utterance_end)
            except Exception:
                pass

            nova3_langs = {
                "en", "es", "fr", "de", "it", "pt", "nl", "hi", "ja",
                "ko", "zh", "sv", "da", "no", "fi", "pl", "ru", "tr",
                "uk", "id", "ms", "th", "vi", "cs", "ro", "hu", "el",
                "bg", "hr", "sk", "sl", "sr", "ca", "ta", "te",
            }
            model = "nova-3" if self._language in nova3_langs else "nova-2"
            logger.info("Deepgram model: %s (language: %s)", model, self._language)

            options = LiveOptions(
                model=model,
                language=self._language,
                smart_format=False,
                no_delay=True,
                interim_results=True,
                endpointing=10,
                utterance_end_ms=1000,
                vad_events=True,
                encoding="linear16",
                channels=1,
                sample_rate=SAMPLE_RATE,
            )

            ok = conn.start(options)
            if not ok:
                logger.error("Deepgram connection failed to start")
                return False

            self._connection = conn
            logger.info("Deepgram connection established")
            return True

        except Exception as exc:
            logger.error("Deepgram connection error: %s", exc)
            return False

    def start(self) -> bool:
        self._stop_event.clear()
        if not self._start_connection():
            return False
        self._thread = threading.Thread(target=self._feed_loop, daemon=True)
        self._thread.start()
        logger.info("Deepgram streaming transcriber started")
        return True

    def stop(self) -> None:
        self._stop_event.set()
        if self._connection:
            try:
                self._connection.finish()
            except Exception:
                pass
            self._connection = None
        if self._thread:
            self._thread.join(timeout=3.0)


class WhisperTranscriber:
    """VAD-based local transcriber using faster-whisper.

    Instead of fixed-time chunks, uses energy-based voice activity detection:
    - Accumulates audio while speech is detected
    - Commits and transcribes immediately when silence follows speech (~0.4s latency)
    - Shows interim text every 1.5s during long speech segments
    - Uses 'base' model for good accuracy/speed balance on CPU
    """

    # VAD tuning
    SPEECH_RMS_THRESHOLD = 0.0015  # RMS amplitude to consider as speech
    SILENCE_COMMIT_SEC = 0.35      # silence duration to commit a segment
    MAX_SEGMENT_SEC = 10.0         # force commit after this long regardless
    FAST_INTERIM_SEC = 0.5         # show a quick preview after this much speech
    INTERIM_SEC = 1.0              # then refresh interim every this many seconds
    MIN_SEGMENT_SEC = 0.3          # ignore segments shorter than this (noise)

    def __init__(
        self,
        audio_capture: "CombinedAudioCapture",
        transcript_buffer: "SentenceBuffer",
        model_name: str = "base",
        language: str = "en",
    ) -> None:
        self._capture = audio_capture
        self._buffer = transcript_buffer
        self._model_name = model_name
        self._language = language
        self._model = None
        self._thread: Optional[threading.Thread] = None
        self._stop_event = threading.Event()

    def _load_model(self):
        if self._model is None:
            logger.info("Loading faster-whisper model '%s'...", self._model_name)
            from meeting_helper.ssl_fix import patch_ssl
            patch_ssl()
            from faster_whisper import WhisperModel
            self._model = WhisperModel(self._model_name, device="cpu", compute_type="int8")
            logger.info("faster-whisper model '%s' loaded", self._model_name)
        return self._model

    def _transcribe(self, audio: np.ndarray) -> str:
        try:
            model = self._load_model()
            segments, _ = model.transcribe(
                audio,
                language=self._language if self._language != "auto" else None,
                beam_size=3,
                condition_on_previous_text=True,
                no_speech_threshold=0.45,
            )
            return " ".join(seg.text.strip() for seg in segments).strip()
        except Exception as exc:
            logger.error("Whisper error: %s", exc)
            return ""

    def _broadcast(self, text: str, interim: bool = False) -> None:
        from meeting_helper.state import state
        loop = state.asyncio_loop
        if loop is not None:
            import asyncio
            asyncio.run_coroutine_threadsafe(
                DeepgramTranscriber._broadcast_transcript(text, interim=interim),
                loop,
            )

    def _commit(self, speech_chunks: list[np.ndarray]) -> None:
        """Transcribe and broadcast a completed speech segment."""
        audio = np.concatenate(speech_chunks)
        if len(audio) < int(self.MIN_SEGMENT_SEC * SAMPLE_RATE):
            return
        text = self._transcribe(audio)
        if text:
            logger.debug("Whisper final: %s", text[:80])
            self._buffer.append_final(text)
            self._broadcast(text, interim=False)

    def _run(self) -> None:
        silence_commit = int(self.SILENCE_COMMIT_SEC * SAMPLE_RATE)
        max_segment = int(self.MAX_SEGMENT_SEC * SAMPLE_RATE)
        fast_interim = int(self.FAST_INTERIM_SEC * SAMPLE_RATE)
        interim_interval = int(self.INTERIM_SEC * SAMPLE_RATE)

        speech_chunks: list[np.ndarray] = []
        speech_samples = 0
        silence_samples = 0
        last_interim_at = 0
        fast_interim_done = False

        while not self._stop_event.is_set():
            chunk = self._capture.get_transcriber_audio(timeout=0.05)
            if chunk is None:
                continue

            rms = float(np.sqrt(np.mean(chunk.astype(np.float32) ** 2)))
            is_speech = rms > self.SPEECH_RMS_THRESHOLD

            if is_speech:
                speech_chunks.append(chunk)
                speech_samples += len(chunk)
                silence_samples = 0

                # Fast preview after FAST_INTERIM_SEC (first interim for short sentences)
                if not fast_interim_done and speech_samples >= fast_interim:
                    audio = np.concatenate(speech_chunks)
                    text = self._transcribe(audio)
                    if text:
                        self._buffer.set_interim(text)
                        self._broadcast(text, interim=True)
                    last_interim_at = speech_samples
                    fast_interim_done = True

                # Regular interim every INTERIM_SEC after the fast one
                elif fast_interim_done and speech_samples - last_interim_at >= interim_interval:
                    audio = np.concatenate(speech_chunks)
                    text = self._transcribe(audio)
                    if text:
                        self._buffer.set_interim(text)
                        self._broadcast(text, interim=True)
                    last_interim_at = speech_samples

                # Force commit on very long segments
                if speech_samples >= max_segment:
                    self._commit(speech_chunks)
                    speech_chunks, speech_samples, silence_samples, last_interim_at, fast_interim_done = [], 0, 0, 0, False

            elif speech_chunks:
                # Silence after speech — keep accumulating silence for context
                speech_chunks.append(chunk)
                silence_samples += len(chunk)

                if silence_samples >= silence_commit:
                    self._commit(speech_chunks)
                    speech_chunks, speech_samples, silence_samples, last_interim_at, fast_interim_done = [], 0, 0, 0, False

        # Flush on stop
        if speech_chunks:
            self._commit(speech_chunks)

    def start(self) -> bool:
        try:
            import faster_whisper  # noqa: F401
        except ImportError:
            logger.warning("faster-whisper not installed — no transcription available.")
            return False
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()
        logger.info("Whisper VAD transcriber started (model=%s, silence_commit=%.2fs)",
                     self._model_name, self.SILENCE_COMMIT_SEC)
        return True

    def stop(self) -> None:
        self._stop_event.set()
        if self._thread:
            self._thread.join(timeout=5.0)


def create_transcriber(
    audio_capture: CombinedAudioCapture,
    transcript_buffer: SentenceBuffer,
    deepgram_api_key: str = "",
    transcription_language: str = "en",
):
    """Return a started transcriber. Prefers Deepgram, falls back to Whisper."""
    if deepgram_api_key:
        t = DeepgramTranscriber(
            audio_capture, transcript_buffer, deepgram_api_key, language=transcription_language
        )
        if t.start():
            return t
        logger.warning("Deepgram failed, falling back to local Whisper")

    t = WhisperTranscriber(audio_capture, transcript_buffer, model_name="base", language=transcription_language)
    if t.start():
        return t

    logger.warning("No transcriber available.")
    return None
