"""Voice transcription providers and factory."""

from __future__ import annotations

import asyncio
import json
import os
import shutil
import subprocess
import tempfile
import wave
from pathlib import Path
from typing import Protocol

import httpx
from loguru import logger


class TranscriptionProvider(Protocol):
    """Common transcription provider protocol."""

    async def transcribe(self, file_path: str | Path) -> str: ...


class GroqTranscriptionProvider:
    """
    Voice transcription provider using Groq's Whisper API.

    Groq offers extremely fast transcription with a generous free tier.
    """

    def __init__(self, api_key: str | None = None):
        self.api_key = api_key or os.environ.get("GROQ_API_KEY")
        self.api_url = "https://api.groq.com/openai/v1/audio/transcriptions"

    async def transcribe(self, file_path: str | Path) -> str:
        """
        Transcribe an audio file using Groq.

        Args:
            file_path: Path to the audio file.

        Returns:
            Transcribed text.
        """
        if not self.api_key:
            logger.warning("Groq API key not configured for transcription")
            return ""

        path = Path(file_path)
        if not path.exists():
            logger.error("Audio file not found: {}", file_path)
            return ""

        try:
            async with httpx.AsyncClient() as client:
                with open(path, "rb") as f:
                    files = {
                        "file": (path.name, f),
                        "model": (None, "whisper-large-v3"),
                    }
                    headers = {
                        "Authorization": f"Bearer {self.api_key}",
                    }

                    response = await client.post(
                        self.api_url,
                        headers=headers,
                        files=files,
                        timeout=60.0
                    )

                    response.raise_for_status()
                    data = response.json()
                    return data.get("text", "")

        except Exception as e:
            logger.error("Groq transcription error: {}", e)
            return ""


class VoskTranscriptionProvider:
    """Offline transcription provider using local Vosk model."""

    def __init__(self, model_path: str | None = None):
        self.model_path = (
            Path(model_path).expanduser()
            if model_path
            else Path.home() / ".miobot" / "models" / "vosk-model-small-cn-0.22"
        )
        self._model = None

    def _load_model(self):
        # 懒加载模型，避免启动时占用过多内存和时间。
        if self._model is not None:
            return self._model
        try:
            from vosk import Model
        except ImportError as e:
            raise RuntimeError("vosk not installed; run: pip install vosk") from e

        if not self.model_path.exists():
            raise RuntimeError(
                f"Vosk model not found: {self.model_path}. "
                "Set MIOBOT_VOSK_MODEL_PATH or place model under ~/.miobot/models/"
            )
        self._model = Model(str(self.model_path))
        return self._model

    @staticmethod
    def _is_pcm_wav(path: Path) -> bool:
        # Vosk 直接识别要求：16kHz / 单声道 / 16-bit PCM WAV。
        try:
            with wave.open(str(path), "rb") as wf:
                return (
                    wf.getnchannels() == 1
                    and wf.getframerate() == 16000
                    and wf.getsampwidth() == 2
                )
        except Exception:
            return False

    @staticmethod
    def _convert_to_pcm_wav(src: Path) -> Path:
        # 非标准音频统一转成 Vosk 友好的 WAV。
        if shutil.which("ffmpeg") is None:
            raise RuntimeError("ffmpeg is required for non-wav audio conversion")
        fd, out = tempfile.mkstemp(prefix="miobot_asr_", suffix=".wav")
        os.close(fd)
        out_path = Path(out)
        cmd = [
            "ffmpeg",
            "-y",
            "-i",
            str(src),
            "-ac",
            "1",
            "-ar",
            "16000",
            "-f",
            "wav",
            str(out_path),
        ]
        proc = subprocess.run(cmd, capture_output=True, text=True)
        if proc.returncode != 0:
            out_path.unlink(missing_ok=True)
            raise RuntimeError(f"ffmpeg convert failed: {proc.stderr[-300:]}")
        return out_path

    def _transcribe_sync(self, wav_path: Path) -> str:
        from vosk import KaldiRecognizer

        model = self._load_model()
        with wave.open(str(wav_path), "rb") as wf:
            rec = KaldiRecognizer(model, wf.getframerate())
            texts: list[str] = []
            while True:
                # 分块喂音频，减少单次处理内存压力。
                data = wf.readframes(4000)
                if len(data) == 0:
                    break
                if rec.AcceptWaveform(data):
                    result = json.loads(rec.Result())
                    if result.get("text"):
                        texts.append(result["text"])
            final = json.loads(rec.FinalResult())
            if final.get("text"):
                texts.append(final["text"])
        return " ".join(t for t in texts if t).strip()

    async def transcribe(self, file_path: str | Path) -> str:
        path = Path(file_path)
        if not path.exists():
            logger.error("Audio file not found: {}", file_path)
            return ""

        tmp: Path | None = None
        wav = path
        try:
            # 输入不是标准 PCM WAV 时先转码，再识别。
            if path.suffix.lower() != ".wav" or not await asyncio.to_thread(self._is_pcm_wav, path):
                tmp = await asyncio.to_thread(self._convert_to_pcm_wav, path)
                wav = tmp
            return await asyncio.to_thread(self._transcribe_sync, wav)
        except Exception as e:
            logger.error("Vosk transcription error: {}", e)
            return ""
        finally:
            if tmp:
                tmp.unlink(missing_ok=True)


class FasterWhisperTranscriptionProvider:
    """Local transcription provider using faster-whisper."""

    def __init__(self, model: str | None = None, compute_type: str | None = None):
        self.model_name = model or os.environ.get("MIOBOT_FASTER_WHISPER_MODEL", "small")
        self.compute_type = compute_type or os.environ.get(
            "MIOBOT_FASTER_WHISPER_COMPUTE_TYPE", "int8"
        )
        self._model = None

    def _load_model(self):
        if self._model is not None:
            return self._model
        try:
            from faster_whisper import WhisperModel
        except ImportError as e:
            raise RuntimeError(
                "faster-whisper not installed; run: pip install faster-whisper"
            ) from e
        # CPU 优先使用 int8，减少内存与算力压力。
        self._model = WhisperModel(self.model_name, device="cpu", compute_type=self.compute_type)
        return self._model

    def _transcribe_sync(self, file_path: Path) -> str:
        model = self._load_model()
        segments, _info = model.transcribe(str(file_path), vad_filter=True, language="zh")
        texts = [seg.text.strip() for seg in segments if seg.text and seg.text.strip()]
        return " ".join(texts).strip()

    async def transcribe(self, file_path: str | Path) -> str:
        path = Path(file_path)
        if not path.exists():
            logger.error("Audio file not found: {}", file_path)
            return ""
        try:
            return await asyncio.to_thread(self._transcribe_sync, path)
        except Exception as e:
            logger.error("faster-whisper transcription error: {}", e)
            return ""


def create_transcription_provider(
    *,
    groq_api_key: str | None = None,
    enabled: bool | None = None,
    provider: str | None = None,
    vosk_model_path: str | None = None,
    faster_whisper_model: str | None = None,
    faster_whisper_compute_type: str | None = None,
) -> TranscriptionProvider | None:
    """Create transcription provider from env config.

    Env:
    - MIOBOT_TRANSCRIPTION_PROVIDER: auto|groq|vosk|faster_whisper|none (default: auto)
    - MIOBOT_VOSK_MODEL_PATH: local Vosk model path
    - MIOBOT_FASTER_WHISPER_MODEL: tiny/base/small/medium/large-v3
    - MIOBOT_FASTER_WHISPER_COMPUTE_TYPE: int8/int8_float16/float16/float32
    """

    if enabled is False:
        return None

    provider_name = (
        provider if provider is not None
        else os.environ.get("MIOBOT_TRANSCRIPTION_PROVIDER", "auto")
    ).strip().lower()
    if provider_name in {"none", "off", "disabled"}:
        return None
    if provider_name == "groq":
        return GroqTranscriptionProvider(api_key=groq_api_key)
    if provider_name in {"faster_whisper", "faster-whisper", "whisper"}:
        return FasterWhisperTranscriptionProvider(
            model=faster_whisper_model,
            compute_type=faster_whisper_compute_type,
        )

    resolved_vosk_model_path = (
        vosk_model_path if vosk_model_path is not None
        else os.environ.get("MIOBOT_VOSK_MODEL_PATH")
    )
    if provider_name == "vosk":
        return VoskTranscriptionProvider(resolved_vosk_model_path)

    # auto 模式：优先云端 Groq；否则优先本地 faster-whisper，再退回 Vosk。
    if groq_api_key or os.environ.get("GROQ_API_KEY"):
        return GroqTranscriptionProvider(api_key=groq_api_key)
    try:
        import faster_whisper  # noqa: F401
        return FasterWhisperTranscriptionProvider(
            model=faster_whisper_model,
            compute_type=faster_whisper_compute_type,
        )
    except Exception:
        pass
    default_vosk_path = (
        Path(resolved_vosk_model_path).expanduser()
        if resolved_vosk_model_path
        else Path.home() / ".miobot" / "models" / "vosk-model-small-cn-0.22"
    )
    if default_vosk_path.exists():
        return VoskTranscriptionProvider(resolved_vosk_model_path)
    # 两者都不可用时返回 None，渠道侧会自动走“无转写”回退逻辑。
    logger.info(
        "No transcription provider available: set GROQ_API_KEY or install Vosk model at {}",
        default_vosk_path,
    )
    return None
