"""Speech-to-text engine using mlx-whisper.

Runs OpenAI Whisper models locally on Apple Silicon via the MLX framework.
Models are downloaded from HuggingFace on first use and cached automatically.
"""

import asyncio
import logging
import os
import tempfile
import wave

import numpy as np

from cadecoder.core.constants import DEFAULT_STT_MODEL
from cadecoder.core.errors import VoiceError
from cadecoder.core.logging import log
from cadecoder.voice.audio import DEFAULT_SAMPLE_RATE


def _suppress_hf_logging() -> None:
    """Suppress noisy HuggingFace Hub download/progress logging.

    Models are cached locally after first download, but HF Hub still logs
    status messages on every load. This silences those for cleaner output.
    """
    # Disable HF Hub progress bars and info logging
    os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
    os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS_WARNING", "1")

    # Set logging levels for noisy libraries
    for logger_name in (
        "huggingface_hub",
        "huggingface_hub.file_download",
        "filelock",
        "urllib3",
        "transformers",
        "mlx_whisper",
    ):
        logging.getLogger(logger_name).setLevel(logging.WARNING)


class WhisperSTT:
    """Local speech-to-text using mlx-whisper on Apple Silicon.

    Transcribes audio arrays into text using a Whisper model running
    natively on the Neural Engine / GPU via MLX.

    Args:
        model: HuggingFace repo ID for the Whisper model.
        language: Language code for transcription (None for auto-detect).
    """

    def __init__(
        self,
        model: str = DEFAULT_STT_MODEL,
        language: str | None = "en",
    ) -> None:
        self.model = model
        self.language = language
        self._mlx_whisper: object | None = None

    def _ensure_loaded(self) -> None:
        """Lazy-load mlx_whisper on first use."""
        if self._mlx_whisper is not None:
            return

        # Suppress HF Hub logging before importing mlx_whisper
        _suppress_hf_logging()

        try:
            import mlx_whisper  # type: ignore[import-untyped]

            self._mlx_whisper = mlx_whisper
            log.info(f"Loaded mlx-whisper with model: {self.model}")
        except ImportError as exc:
            raise VoiceError(
                "mlx-whisper is required for voice mode. "
                "Install it with: pip install 'cade-cli[voice]'"
            ) from exc

    async def transcribe(
        self,
        audio: np.ndarray,
        sample_rate: int = DEFAULT_SAMPLE_RATE,
    ) -> str:
        """Transcribe an audio array to text.

        Args:
            audio: 1-D float32 numpy array of audio samples.
            sample_rate: Sample rate of the audio data.

        Returns:
            Transcribed text string.

        Raises:
            VoiceError: If transcription fails.
        """
        if audio.size == 0:
            return ""

        self._ensure_loaded()
        loop = asyncio.get_running_loop()
        return await loop.run_in_executor(None, self._transcribe_blocking, audio, sample_rate)

    def _transcribe_blocking(self, audio: np.ndarray, sample_rate: int) -> str:
        """Run transcription in a thread (blocking)."""
        if self._mlx_whisper is None:
            raise VoiceError("STT model not loaded — call _ensure_loaded() first")

        try:
            # mlx_whisper.transcribe accepts a file path or numpy array.
            # Write to a temp WAV file for maximum compatibility across
            # mlx-whisper versions.
            with tempfile.NamedTemporaryFile(suffix=".wav", delete=True) as tmp:
                self._write_wav(tmp.name, audio, sample_rate)
                result = self._mlx_whisper.transcribe(  # type: ignore[union-attr]
                    tmp.name,
                    path_or_hf_repo=self.model,
                    language=self.language,
                    fp16=False,
                )

            text = result.get("text", "").strip()
            log.debug(f"Transcribed {len(audio) / sample_rate:.1f}s -> '{text[:80]}'")
            return text

        except Exception as exc:
            raise VoiceError(f"Transcription failed: {exc}") from exc

    @staticmethod
    def _write_wav(path: str, audio: np.ndarray, sample_rate: int) -> None:
        """Write a float32 numpy array to a 16-bit PCM WAV file."""
        # Convert float32 [-1.0, 1.0] to int16
        pcm = (audio * 32767).clip(-32768, 32767).astype(np.int16)
        with wave.open(path, "wb") as wf:
            wf.setnchannels(1)
            wf.setsampwidth(2)  # 16-bit
            wf.setframerate(sample_rate)
            wf.writeframes(pcm.tobytes())
