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

Deepgram: ~200ms latency, real-time streaming via WebSocket.
Fallback: faster-whisper on 10-second chunks when no Deepgram key.

Must be imported only after daemonize() — same constraint as audio/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 ai_interview.audio.capture import CombinedAudioCapture
    from ai_interview.buffer import RollingTranscriptBuffer

logger = logging.getLogger(__name__)

SAMPLE_RATE = 16000


# ---------------------------------------------------------------------------
# Deepgram streaming transcriber
# ---------------------------------------------------------------------------

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

    def __init__(
        self,
        audio_capture: "CombinedAudioCapture",
        transcript_buffer: "RollingTranscriptBuffer",
        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

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

            from ai_interview.state import state
            loop = state.asyncio_loop

            if result.is_final:
                # Final: add to AI buffer and broadcast; clear pending interim
                logger.info("Deepgram final: %s", sentence[:120])
                self._last_interim = None
                self._buffer.append(sentence)
                if loop is not None:
                    import asyncio
                    asyncio.run_coroutine_threadsafe(
                        self._broadcast_transcript(sentence, interim=False), loop
                    )
            else:
                # Interim: track for utterance_end flush; broadcast to transcript bar
                self._last_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 ai_interview.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):
        """Fired when a long pause is detected — flush last interim as final."""
        try:
            last = getattr(self, '_last_interim', None)
            if last:
                logger.info("Deepgram utterance_end flush: %s", last[:80])
                self._buffer.append(last)
                from ai_interview.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):
        """Fired immediately when Deepgram detects voice — before any words."""
        from ai_interview.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 ai_interview.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)
        # Trigger reconnect on 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()

    _reconnect_attempts: int = 0

    def _reconnect(self) -> None:
        """Re-establish Deepgram connection after a drop with exponential backoff."""
        if self._stop_event.is_set():
            return

        self._reconnect_attempts += 1
        delay = min(2 ** self._reconnect_attempts, 30)  # 2, 4, 8, 16, 30, 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
            logger.info("Deepgram reconnected successfully")
        elif self._reconnect_attempts < 10:
            threading.Thread(target=self._reconnect, daemon=True).start()
        else:
            logger.error("Deepgram reconnect failed after %d attempts — giving up", self._reconnect_attempts)

    def _feed_loop(self):
        """Read audio chunks and send to Deepgram connection."""
        last_keepalive = time.time()
        while not self._stop_event.is_set():
            chunk = self._capture.get_audio(timeout=0.01)  # 10ms poll
            if self._connection is None:
                time.sleep(0.1)
                continue
            if chunk is None:
                # Send keepalive every 8s to prevent idle disconnect
                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)

    def _start_connection(self) -> bool:
        """Open a new Deepgram live connection and register callbacks."""
        try:
            import ssl, certifi, 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

            # Nova-3 supports: en, es, fr, de, it, pt, nl, hi, ja, ko, zh, and more
            # See: https://developers.deepgram.com/docs/models-languages-overview
            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,     # smart_format adds up to 3s latency waiting for entities
                no_delay=True,          # return results immediately, no formatting wait
                interim_results=True,
                endpointing=10,         # 10ms — fastest silence detection (Deepgram default)
                utterance_end_ms=1000,  # force-finalize if pause >= 1s mid-sentence
                vad_events=True,
                encoding="linear16",
                channels=1,
                sample_rate=SAMPLE_RATE,
            )

            if not conn.start(options):
                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)


# ---------------------------------------------------------------------------
# Local Whisper fallback transcriber (10-second chunks)
# ---------------------------------------------------------------------------

class WhisperTranscriber:
    """Transcribes audio locally with faster-whisper in 10-second chunks.

    Used as fallback when no Deepgram key is available.
    """

    CHUNK_SECONDS = 10

    def __init__(
        self,
        audio_capture: "CombinedAudioCapture",
        transcript_buffer: "RollingTranscriptBuffer",
        model_name: str = "base",
    ) -> None:
        self._capture = audio_capture
        self._buffer = transcript_buffer
        self._model_name = model_name
        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 Whisper model '%s'…", self._model_name)
            try:
                from faster_whisper import WhisperModel
            except ImportError:
                raise RuntimeError(
                    "faster-whisper not installed. Run: pip install faster-whisper"
                )
            self._model = WhisperModel(self._model_name, device="cpu", compute_type="int8")
            logger.info("Whisper model loaded")
        return self._model

    def _transcribe(self, audio: np.ndarray) -> str:
        try:
            model = self._load_model()
            segments, _ = model.transcribe(audio, language="en")
            return " ".join(seg.text.strip() for seg in segments).strip()
        except RuntimeError:
            raise  # propagate import error to stop the loop
        except Exception as exc:
            logger.error("Whisper error: %s", exc)
            return ""

    def _run(self) -> None:
        chunk_samples = self.CHUNK_SECONDS * SAMPLE_RATE
        accumulated: list[np.ndarray] = []
        total = 0

        while not self._stop_event.is_set():
            chunk = self._capture.get_audio(timeout=0.1)
            if chunk is None:
                continue
            accumulated.append(chunk)
            total += len(chunk)
            if total >= chunk_samples:
                audio = np.concatenate(accumulated)
                accumulated, total = [], 0
                text = self._transcribe(audio)
                if text:
                    logger.debug("Whisper: %s", text[:80])
                    self._buffer.append(text)

        if accumulated:
            audio = np.concatenate(accumulated)
            if len(audio) > SAMPLE_RATE:
                text = self._transcribe(audio)
                if text:
                    self._buffer.append(text)

    def start(self) -> bool:
        # Verify faster-whisper is available before starting the thread
        try:
            import faster_whisper  # noqa: F401
        except ImportError:
            logger.warning(
                "faster-whisper not installed — no transcription available. "
                "Either add a Deepgram key (ai-interview setup) or run: pip install faster-whisper"
            )
            return False

        self._stop_event.clear()
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()
        logger.info("Whisper fallback transcriber started (chunk=%ds)", self.CHUNK_SECONDS)
        return True

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


# ---------------------------------------------------------------------------
# Factory: pick Deepgram or Whisper based on available key
# ---------------------------------------------------------------------------

def create_transcriber(
    audio_capture: "CombinedAudioCapture",
    transcript_buffer: "RollingTranscriptBuffer",
    deepgram_api_key: str = "",
    transcription_language: str = "en",
):
    """Return a started transcriber. Prefers Deepgram, falls back to Whisper.

    Returns None if neither is available — the assistant still works,
    just without audio context (you can still use screenshots + ESC+↑).
    """
    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)
    if t.start():
        return t

    logger.warning(
        "No transcriber available. Assistant works via screenshots only. "
        "Add a Deepgram key with: ai-interview setup"
    )
    return None
