"""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
        self._reconnect_lock = threading.Lock()
        self._reconnect_attempts = 0
        self._last_transcript_time: float = 0.0
        self._last_interim = 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)
        self._trigger_reconnect()

    def _on_close(self, sender, close=None, **kwargs):
        logger.warning("Deepgram connection closed — reconnecting…")
        self._trigger_reconnect()

    def _trigger_reconnect(self) -> None:
        """Ensure only one reconnect thread is running at a time."""
        if self._stop_event.is_set():
            return
        if self._reconnect_lock.locked():
            return  # reconnect already in progress
        threading.Thread(target=self._reconnect, daemon=True).start()

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

        Uses _reconnect_lock to ensure only one reconnect loop runs at a time.
        Loops internally instead of spawning new threads on each attempt.
        """
        if not self._reconnect_lock.acquire(blocking=False):
            return  # another thread already owns the reconnect loop

        try:
            for attempt in range(1, 11):
                if self._stop_event.is_set():
                    return

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

                if self._stop_event.is_set():
                    return

                # Close stale connection
                conn = self._connection
                self._connection = None
                if conn is not None:
                    try:
                        conn.finish()
                    except Exception:
                        pass

                if self._start_connection():
                    try:
                        from ai_interview.metrics import metrics
                        metrics.record("deepgram_reconnect", attempt=attempt, ok=True)
                    except Exception:
                        pass
                    self._reconnect_attempts = 0
                    self._last_transcript_time = time.time()
                    logger.info("Deepgram reconnected successfully (attempt %d)", attempt)
                    self._thread = threading.Thread(target=self._feed_loop, daemon=True)
                    self._thread.start()
                    return
                else:
                    try:
                        from ai_interview.metrics import metrics
                        metrics.record("deepgram_reconnect", attempt=attempt, ok=False)
                    except Exception:
                        pass

            logger.error("Deepgram reconnect failed after 10 attempts — falling back to local Whisper")
            self._start_whisper_fallback()
        finally:
            self._reconnect_lock.release()

    def _start_whisper_fallback(self) -> None:
        """Start local Whisper transcription after Deepgram gives up."""
        try:
            fallback = WhisperTranscriber(self._capture, self._buffer)
            if fallback.start():
                logger.info("Whisper fallback transcriber started after Deepgram failure")
            else:
                logger.warning("Whisper fallback also unavailable — no transcription")
        except Exception as exc:
            logger.error("Failed to start Whisper fallback: %s", exc)

    def _feed_loop(self):
        """Read audio chunks and send to Deepgram connection."""
        last_keepalive = time.time()
        self._last_transcript_time = time.time()
        _WATCHDOG_TIMEOUT = 30  # reconnect if no transcript for 30s while sending audio
        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)
                self._trigger_reconnect()
                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))
                    self._trigger_reconnect()
                    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",
            }
            # Deepgram doesn't support auto-detect — fall back to English
            if self._language == "auto":
                self._language = "en"
            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 (VAD-based, ported from meeting-helper)
# ---------------------------------------------------------------------------

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

    Uses energy-based voice activity detection instead of fixed-time chunks:
    - Accumulates audio while speech is detected (RMS threshold)
    - Commits and transcribes immediately when silence follows speech (~0.35s latency)
    - Shows interim text every 1s 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: "RollingTranscriptBuffer",
        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 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()
            t0 = time.monotonic()
            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,
            )
            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 Exception as exc:
            logger.error("Whisper error: %s", exc)
            return ""

    def _broadcast(self, text: str, interim: bool = False) -> None:
        from ai_interview.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) -> None:
        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(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 = []
        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_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
                if not fast_interim_done and speech_samples >= fast_interim:
                    text = self._transcribe(np.concatenate(speech_chunks))
                    if text:
                        self._broadcast(text, interim=True)
                    last_interim_at = speech_samples
                    fast_interim_done = True

                # Regular interim refresh every INTERIM_SEC
                elif fast_interim_done and speech_samples - last_interim_at >= interim_interval:
                    text = self._transcribe(np.concatenate(speech_chunks))
                    if 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 = [], 0, 0
                    last_interim_at, fast_interim_done = 0, False

            elif speech_chunks:
                # Silence after speech — accumulate 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 = [], 0, 0
                    last_interim_at, fast_interim_done = 0, False

        # Flush remaining 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. "
                "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 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)


# ---------------------------------------------------------------------------
# Remote Whisper transcriber (faster-whisper-server over HTTP)
# ---------------------------------------------------------------------------

class RemoteWhisperTranscriber:
    """VAD-based transcriber that sends audio to a remote faster-whisper-server.

    Same VAD logic as WhisperTranscriber, but instead of loading a local model,
    POSTs WAV audio to a remote server's OpenAI-compatible endpoint:
        POST {host}/v1/audio/transcriptions
    """

    SPEECH_RMS_THRESHOLD = 0.0015
    SILENCE_COMMIT_SEC   = 0.35
    MAX_SEGMENT_SEC      = 10.0
    FAST_INTERIM_SEC     = 0.5     # first preview after 0.5s of speech
    INTERIM_SEC          = 1.0     # then refresh every 1s
    MIN_SEGMENT_SEC      = 0.3

    def __init__(
        self,
        audio_capture: "CombinedAudioCapture",
        transcript_buffer: "RollingTranscriptBuffer",
        host: str,
        language: str = "en",
    ) -> None:
        self._capture = audio_capture
        self._buffer = transcript_buffer
        self._host = host.rstrip("/")
        self._language = language
        self._thread: Optional[threading.Thread] = None
        self._stop_event = threading.Event()
        self._conn = None  # persistent HTTP connection

    def _get_conn(self):
        """Return a persistent HTTP connection, reconnecting if needed."""
        import http.client
        from urllib.parse import urlparse
        if self._conn is not None:
            return self._conn
        parsed = urlparse(self._host)
        self._conn = http.client.HTTPConnection(
            parsed.hostname, parsed.port or 8000, timeout=30,
        )
        return self._conn

    def _transcribe_remote(self, audio: np.ndarray) -> str:
        """Send audio to remote faster-whisper-server and return text."""
        import io
        import json
        import struct

        # Build WAV in memory
        num_samples = len(audio)
        pcm = (audio * 32767).astype(np.int16)
        data_size = num_samples * 2
        wav_buf = io.BytesIO()
        wav_buf.write(b"RIFF")
        wav_buf.write(struct.pack("<I", 36 + data_size))
        wav_buf.write(b"WAVEfmt ")
        wav_buf.write(struct.pack("<IHHIIHH", 16, 1, 1, SAMPLE_RATE, SAMPLE_RATE * 2, 2, 16))
        wav_buf.write(b"data")
        wav_buf.write(struct.pack("<I", data_size))
        wav_buf.write(pcm.tobytes())
        wav_data = wav_buf.getvalue()

        # Multipart form data
        boundary = "----WhisperBoundary"
        body = b""
        body += f"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"audio.wav\"\r\nContent-Type: audio/wav\r\n\r\n".encode()
        body += wav_data + b"\r\n"
        if self._language and self._language != "auto":
            body += f"--{boundary}\r\nContent-Disposition: form-data; name=\"language\"\r\n\r\n{self._language}\r\n".encode()
        body += f"--{boundary}--\r\n".encode()

        try:
            t0 = time.monotonic()
            headers = {"Content-Type": f"multipart/form-data; boundary={boundary}"}

            # Try persistent connection, reconnect on failure
            for attempt in range(2):
                try:
                    conn = self._get_conn()
                    conn.request("POST", "/v1/audio/transcriptions", body=body, headers=headers)
                    resp = conn.getresponse()
                    break
                except Exception:
                    self._conn = None
                    if attempt == 1:
                        raise

            resp_data = resp.read()
            if resp.status != 200:
                logger.error("Remote whisper %d: %s", resp.status, resp_data[:200])
                return ""

            text = json.loads(resp_data).get("text", "").strip()
            try:
                from ai_interview.metrics import metrics
                metrics.record("whisper_remote_transcribe",
                               val=int((time.monotonic() - t0) * 1000),
                               audio_seconds=round(num_samples / SAMPLE_RATE, 1),
                               chars=len(text))
            except Exception:
                pass
            return text
        except Exception as exc:
            logger.error("Remote whisper error (%s): %s", self._host, exc)
            return ""

    def _broadcast(self, text: str, interim: bool = False) -> None:
        from ai_interview.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) -> None:
        audio = np.concatenate(speech_chunks)
        if len(audio) < int(self.MIN_SEGMENT_SEC * SAMPLE_RATE):
            return
        text = self._transcribe_remote(audio)
        if text:
            logger.debug("Remote Whisper final: %s", text[:80])
            self._buffer.append(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 = []
        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_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
                if not fast_interim_done and speech_samples >= fast_interim:
                    text = self._transcribe_remote(np.concatenate(speech_chunks))
                    if text:
                        self._broadcast(text, interim=True)
                    last_interim_at = speech_samples
                    fast_interim_done = True

                # Regular interim refresh every INTERIM_SEC
                elif fast_interim_done and speech_samples - last_interim_at >= interim_interval:
                    text = self._transcribe_remote(np.concatenate(speech_chunks))
                    if 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 = [], 0, 0
                    last_interim_at, fast_interim_done = 0, False

            elif speech_chunks:
                speech_chunks.append(chunk)
                silence_samples += len(chunk)

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

        if speech_chunks:
            self._commit(speech_chunks)

    def start(self) -> bool:
        # Verify server is reachable
        try:
            import urllib.request
            urllib.request.urlopen(f"{self._host}/health", timeout=3)
        except Exception:
            try:
                # Some servers don't have /health, try root
                import urllib.request
                urllib.request.urlopen(self._host, timeout=3)
            except Exception:
                logger.error("Remote whisper server not reachable at %s", self._host)
                return False

        self._stop_event.clear()
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()
        logger.info("Remote Whisper transcriber started (host=%s)", self._host)
        return True

    def stop(self) -> None:
        self._stop_event.set()
        if self._thread:
            self._thread.join(timeout=5.0)
        if self._conn:
            try:
                self._conn.close()
            except Exception:
                pass
            self._conn = None


# ---------------------------------------------------------------------------
# Factory: pick Deepgram, Remote Whisper, or Local Whisper
# ---------------------------------------------------------------------------

def create_transcriber(
    audio_capture: "CombinedAudioCapture",
    transcript_buffer: "RollingTranscriptBuffer",
    deepgram_api_key: str = "",
    transcription_language: str = "en",
    preferred_transcriber: str = "",
    whisper_host: str = "",
):
    """Return a started transcriber.

    preferred_transcriber: "" or "auto" = Deepgram if key, else Whisper.
    "deepgram" = force Deepgram (fails if no key). "whisper" = local or remote Whisper.
    whisper_host: if set, use remote faster-whisper-server instead of local model.

    Returns None if neither is available — the assistant still works,
    just without audio context (you can still use screenshots + ESC+↑).
    """
    pref = preferred_transcriber.lower().strip()

    def _make_whisper():
        """Create the best available Whisper transcriber (remote or local)."""
        if whisper_host:
            t = RemoteWhisperTranscriber(audio_capture, transcript_buffer, host=whisper_host, language=transcription_language)
            if t.start():
                return t
            logger.warning("Remote whisper at %s failed, falling back to local", whisper_host)
        t = WhisperTranscriber(audio_capture, transcript_buffer, language=transcription_language)
        if t.start():
            return t
        return None

    # Force Whisper — skip Deepgram entirely
    if pref == "whisper":
        t = _make_whisper()
        if t:
            return t
        logger.warning("Whisper failed to start")
        return None

    # Try Deepgram (auto or forced)
    if deepgram_api_key and pref != "whisper":
        t = DeepgramTranscriber(audio_capture, transcript_buffer, deepgram_api_key, language=transcription_language)
        if t.start():
            return t
        if pref == "deepgram":
            logger.error("Deepgram forced but failed to start")
            return None
        logger.warning("Deepgram failed, falling back to Whisper")

    # Whisper fallback (auto mode)
    t = _make_whisper()
    if t:
        return t

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