"""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
            self._last_transcript_time = time.time()
            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)
                state.last_activity_at = time.time()
                try:
                    from ai_interview.metrics import metrics
                    metrics.record("deepgram_final", val=len(sentence))
                except Exception:
                    pass
                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():
            try:
                from ai_interview.metrics import metrics
                metrics.record("deepgram_reconnect", attempt=self._reconnect_attempts, ok=True)
            except Exception:
                pass
            self._reconnect_attempts = 0
            self._last_transcript_time = time.time()
            logger.info("Deepgram reconnected successfully")
            # Restart feed loop
            self._thread = threading.Thread(target=self._feed_loop, daemon=True)
            self._thread.start()
        elif self._reconnect_attempts < 10:
            try:
                from ai_interview.metrics import metrics
                metrics.record("deepgram_reconnect", attempt=self._reconnect_attempts, ok=False)
            except Exception:
                pass
            threading.Thread(target=self._reconnect, daemon=True).start()
        else:
            try:
                from ai_interview.metrics import metrics
                metrics.record("deepgram_reconnect", attempt=self._reconnect_attempts, ok=False)
            except Exception:
                pass
            logger.error("Deepgram reconnect failed after %d attempts — giving up", self._reconnect_attempts)

    _last_transcript_time: float = 0.0  # updated on every transcript callback

    def _feed_loop(self):
        """Read audio chunks and send to Deepgram connection."""
        last_keepalive = time.time()
        self._last_transcript_time = time.time()
        _WATCHDOG_TIMEOUT = 60  # if no transcript for 60s while sending audio, reconnect
        last_watchdog_check = 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)
                if not self._stop_event.is_set():
                    threading.Thread(target=self._reconnect, daemon=True).start()
                break

            # Watchdog: if we're sending audio but getting no transcripts, connection is dead
            now = time.time()
            if now - last_watchdog_check > 10:
                last_watchdog_check = now
                silence_duration = now - self._last_transcript_time
                if silence_duration > _WATCHDOG_TIMEOUT:
                    logger.warning("Deepgram watchdog: no transcript for %ds — reconnecting", int(silence_duration))
                    threading.Thread(target=self._reconnect, daemon=True).start()
                    break

    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)

            # Technical terms boosted for coding interviews — no latency impact,
            # processed in the same transcription pass.
            # Nova-3 uses `keyterm`, Nova-2 uses `keywords` with optional weight suffix.
            _TECH_TERMS = [
                # Data structures
                "HashMap", "HashSet", "LinkedList", "ArrayList", "TreeMap", "TreeSet",
                "binary tree", "binary search tree", "trie", "heap", "min-heap", "max-heap",
                "priority queue", "deque", "stack", "queue", "graph", "adjacency list",
                "adjacency matrix", "union find", "disjoint set",
                # Algorithms
                "binary search", "depth first search", "breadth first search",
                "dynamic programming", "memoization", "backtracking", "sliding window",
                "two pointers", "divide and conquer", "greedy", "topological sort",
                "Dijkstra", "Bellman-Ford", "Kruskal", "Prim",
                "quicksort", "mergesort", "heapsort", "radix sort",
                # Complexity
                "O of n", "O of log n", "O of n log n", "O of n squared",
                "time complexity", "space complexity", "amortized",
                # System design
                "load balancer", "reverse proxy", "API gateway", "microservices",
                "Kubernetes", "Docker", "Redis", "Kafka", "RabbitMQ",
                "PostgreSQL", "MongoDB", "Cassandra", "DynamoDB", "Elasticsearch",
                "CDN", "DNS", "REST", "GraphQL", "gRPC", "WebSocket",
                "sharding", "replication", "partitioning", "consistent hashing",
                "CAP theorem", "eventual consistency", "idempotent",
                "rate limiting", "circuit breaker", "saga pattern",
                # Languages / frameworks
                "TypeScript", "JavaScript", "Python", "Golang", "Rust", "Kotlin",
                "React", "Node.js", "Spring Boot", "FastAPI", "Next.js",
                # General tech
                "LRU cache", "LFU cache", "mutex", "semaphore", "deadlock",
                "race condition", "async", "await", "concurrency", "multithreading",
                "garbage collection", "pointer", "recursion", "iteration",
            ]

            if model == "nova-3":
                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,
                    keyterm=_TECH_TERMS,
                )
            else:
                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,
                    keywords=[f"{t}:1.5" for t in _TECH_TERMS],
                )

            t0 = time.monotonic()
            ok = conn.start(options)
            duration_ms = int((time.monotonic() - t0) * 1000)

            if not ok:
                logger.error("Deepgram connection failed to start")
                try:
                    from ai_interview.metrics import metrics
                    metrics.record("deepgram_connect", val=duration_ms, ok=False)
                except Exception:
                    pass
                return False

            self._connection = conn
            logger.info("Deepgram connection established")
            try:
                from ai_interview.metrics import metrics
                metrics.record("deepgram_connect", val=duration_ms, ok=True)
            except Exception:
                pass
            return True

        except Exception as exc:
            logger.error("Deepgram connection error: %s", exc)
            try:
                from ai_interview.metrics import metrics
                metrics.record("deepgram_connect", val=0, ok=False)
            except Exception:
                pass
            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()
            t0 = time.monotonic()
            segments, _ = model.transcribe(audio, language="en")
            text = " ".join(seg.text.strip() for seg in segments).strip()
            try:
                from ai_interview.metrics import metrics
                metrics.record("whisper_transcribe",
                               val=int((time.monotonic() - t0) * 1000),
                               audio_seconds=round(len(audio) / SAMPLE_RATE, 1),
                               chars=len(text))
            except Exception:
                pass
            return text
        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
