"""Audio transcription — converts voice messages to text.

Backend priority:
  1. OpenAI Whisper API (cloud, best latency, needs API key)
  2. LiteLLM transcription (routes to configured provider)
  3. faster-whisper (local, CPU-friendly, ~100MB, no PyTorch)
  4. openai-whisper (local, heavier, requires PyTorch)

Provider-independent: the transcription result is just text.

Sentinel returned when no backend is installed/reachable:
``NO_BACKEND_AVAILABLE``. Distinguishes the "install a backend" case from
the "backend ran but produced no text" case (e.g. silent or non-speech audio).
"""

from __future__ import annotations

import asyncio
import base64
import logging
import tempfile
from pathlib import Path

log = logging.getLogger(__name__)

NO_BACKEND_AVAILABLE = (
    "[Voice message received but could not be transcribed. "
    "No transcription service available.]"
)

# Cached local model handles — loaded on first use (the model files
# are a few hundred MB and take seconds to load).
_faster_whisper_model = None
_openai_whisper_model = None


async def transcribe_audio(
    audio_base64: str,
    mimetype: str = "audio/ogg",
    api_key: str | None = None,
) -> str:
    """Transcribe base64-encoded audio to text.

    Tries OpenAI Whisper API first, falls back to local whisper.
    Returns the transcribed text, or an error message.
    """
    # Decode audio
    audio_bytes = base64.b64decode(audio_base64)

    # Determine file extension from mimetype
    ext_map = {
        "audio/ogg": ".ogg",
        "audio/ogg; codecs=opus": ".ogg",
        "audio/mpeg": ".mp3",
        "audio/mp4": ".m4a",
        "audio/wav": ".wav",
        "audio/webm": ".webm",
    }
    ext = ext_map.get(mimetype, ".ogg")

    # Write to temp file (APIs need a file)
    with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as f:
        f.write(audio_bytes)
        temp_path = Path(f.name)

    try:
        # Try OpenAI Whisper API
        if api_key:
            try:
                return await _transcribe_openai(temp_path, api_key)
            except Exception as e:
                log.warning("OpenAI transcription failed: %s", e)

        # Try LiteLLM transcription (supports multiple providers)
        try:
            return await _transcribe_litellm(temp_path)
        except Exception as e:
            log.debug("LiteLLM transcription failed: %s", e)

        # Try faster-whisper (preferred local backend — smaller, faster)
        try:
            return await asyncio.to_thread(_transcribe_faster_whisper, temp_path)
        except ImportError:
            log.debug("faster-whisper not installed")
        except Exception as e:
            log.warning("faster-whisper failed: %s", e)

        # Try openai-whisper (heavier fallback)
        try:
            return await asyncio.to_thread(_transcribe_local, temp_path)
        except ImportError:
            pass
        except Exception as e:
            log.warning("Local whisper failed: %s", e)

        return NO_BACKEND_AVAILABLE
    finally:
        temp_path.unlink(missing_ok=True)


async def _transcribe_openai(path: Path, api_key: str) -> str:
    """Transcribe using OpenAI Whisper API."""
    from openai import AsyncOpenAI

    client = AsyncOpenAI(api_key=api_key)
    with open(path, "rb") as f:
        response = await client.audio.transcriptions.create(
            model="whisper-1",
            file=f,
        )
    text = response.text.strip()
    log.info("Transcribed %s via OpenAI Whisper (%d chars)", path.suffix, len(text))
    return text


async def _transcribe_litellm(path: Path) -> str:
    """Transcribe using LiteLLM (routes to configured provider)."""
    import litellm

    with open(path, "rb") as f:
        response = await litellm.atranscription(
            model="whisper-1",
            file=f,
        )
    text = response.text.strip()
    log.info("Transcribed %s via LiteLLM (%d chars)", path.suffix, len(text))
    return text


def transcribe_buffer_sync(audio_bytes: bytes, suffix: str = ".webm") -> str:
    """Synchronously transcribe a raw audio buffer with the cached local model.

    Used by the streaming WebSocket handler, which re-runs the model on a
    growing buffer every ~1s. Blocking — callers MUST offload to a thread
    via ``asyncio.to_thread``. Returns empty string if faster-whisper
    isn't installed (streaming has no cloud fallback — it would be too
    slow and too expensive to call the OpenAI API every second).
    """
    try:
        from faster_whisper import WhisperModel  # noqa: F401
    except ImportError:
        return ""

    with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
        f.write(audio_bytes)
        temp_path = Path(f.name)
    try:
        return _transcribe_faster_whisper(temp_path)
    finally:
        temp_path.unlink(missing_ok=True)


def _transcribe_faster_whisper(path: Path) -> str:
    """Transcribe using the faster-whisper library (CTranslate2 backend).

    Preferred local backend: ~4x faster than openai-whisper on CPU, ~100MB
    install vs. ~1GB for the openai-whisper + PyTorch stack, and the
    multilingual ``base`` model is only ~140MB on disk.
    """
    global _faster_whisper_model
    from faster_whisper import WhisperModel

    if _faster_whisper_model is None:
        # ``base`` model — good accuracy for conversational input, fits in
        # ~140MB of RAM and transcribes ~5x realtime on modern CPUs.
        # Use int8 quantisation for lower memory footprint.
        _faster_whisper_model = WhisperModel("base", device="cpu", compute_type="int8")
        log.info("Loaded faster-whisper model (base, int8, CPU)")

    segments, info = _faster_whisper_model.transcribe(str(path), beam_size=5, vad_filter=True)
    text = " ".join(seg.text.strip() for seg in segments).strip()
    log.info(
        "Transcribed %s via faster-whisper (%s, %.1fs audio, %d chars)",
        path.suffix, info.language, info.duration, len(text),
    )
    return text


def _transcribe_local(path: Path) -> str:
    """Transcribe using the openai-whisper library (heavier fallback)."""
    global _openai_whisper_model
    import whisper

    if _openai_whisper_model is None:
        _openai_whisper_model = whisper.load_model("base")
    result = _openai_whisper_model.transcribe(str(path))
    text = result["text"].strip()
    log.info("Transcribed %s locally via whisper (%d chars)", path.suffix, len(text))
    return text
