from __future__ import annotations

import asyncio
import logging
import os
import uuid
from typing import Mapping, Optional, Protocol, cast, overload

from ..core.circuit_breaker import CircuitBreaker
from ..core.exceptions import CodexError, PermanentError, TransientError
from .capture import CaptureCallbacks, CaptureState, PushToTalkCapture
from .config import VoiceConfig
from .provider import SpeechSessionMetadata
from .provider_catalog import local_voice_provider_spec, normalize_voice_provider
from .resolver import resolve_speech_provider


class VoiceServiceError(CodexError):
    """Raised when voice transcription fails at service boundary."""

    def __init__(
        self, reason: str, detail: str, *, user_message: Optional[str] = None
    ) -> None:
        super().__init__(detail, user_message=user_message or detail)
        self.reason = reason
        self.detail = detail


class VoiceTransientError(VoiceServiceError, TransientError):
    """Transient voice errors (rate limits, network issues)."""

    pass


class VoicePermanentError(VoiceServiceError, PermanentError):
    """Permanent voice errors (auth, invalid config)."""

    pass


class ProviderResolver(Protocol):
    @overload
    def __call__(self, config: VoiceConfig) -> object: ...

    @overload
    def __call__(
        self,
        config: VoiceConfig,
        *,
        logger: Optional[logging.Logger],
    ) -> object: ...

    @overload
    def __call__(
        self,
        config: VoiceConfig,
        *,
        logger: Optional[logging.Logger],
        env: Optional[Mapping[str, str]],
    ) -> object: ...


DEFAULT_PROVIDER_RESOLVER = cast(ProviderResolver, resolve_speech_provider)


class VoiceService:
    """
    Thin wrapper that wires the shared PushToTalkCapture into HTTP handlers.
    This keeps raw audio in-memory only and centralizes provider wiring/error mapping.
    """

    def __init__(
        self,
        config: VoiceConfig,
        logger: Optional[logging.Logger] = None,
        provider_resolver: ProviderResolver = DEFAULT_PROVIDER_RESOLVER,
        provider: Optional[object] = None,
        env: Optional[dict] = None,
    ) -> None:
        self.config = config
        self._logger = logger or logging.getLogger(__name__)
        self._provider_resolver = provider_resolver
        self._provider = provider
        self._env = env if env is not None else os.environ
        self._circuit_breaker = CircuitBreaker("Voice", logger=self._logger)

    def config_payload(self) -> dict:
        """Expose safe config fields to the UI."""
        configured_provider = self._configured_provider_name()
        try:
            effective_provider = self.effective_provider_name()
        except Exception:
            effective_provider = configured_provider

        # Providers that do not use remote APIs may not require any API key.
        provider_cfg = self.config.providers.get(effective_provider, {})
        api_key_env = provider_cfg.get("api_key_env")
        api_key_env_name = api_key_env.strip() if isinstance(api_key_env, str) else ""
        requires_api_key = bool(api_key_env_name)
        has_api_key = (
            bool(self._env.get(api_key_env_name)) if requires_api_key else True
        )

        return {
            "enabled": self.config.enabled,
            "provider": self.config.provider,
            "effective_provider": effective_provider,
            "latency_mode": self.config.latency_mode,
            "chunk_ms": self.config.chunk_ms,
            "sample_rate": self.config.sample_rate,
            "warn_on_remote_api": self.config.warn_on_remote_api,
            "has_api_key": has_api_key,
            "api_key_env": api_key_env_name if requires_api_key else None,
            "push_to_talk": {
                "max_ms": self.config.push_to_talk.max_ms,
                "silence_auto_stop_ms": self.config.push_to_talk.silence_auto_stop_ms,
                "min_hold_ms": self.config.push_to_talk.min_hold_ms,
            },
        }

    def transcribe(
        self,
        audio_bytes: bytes,
        *,
        client: str = "web",
        user_agent: Optional[str] = None,
        language: Optional[str] = None,
        filename: Optional[str] = None,
        content_type: Optional[str] = None,
    ) -> dict:
        if not self.config.enabled:
            raise VoiceServiceError("disabled", "Voice is disabled")
        if not audio_bytes:
            raise VoiceServiceError("empty_audio", "No audio received")

        try:
            provider = self._resolve_provider()
        except ValueError as exc:
            raise self._provider_resolution_error(exc) from exc
        buffer = _TranscriptionBuffer()
        capture = PushToTalkCapture(
            provider=provider,
            config=self.config,
            callbacks=buffer.callbacks,
            permission_requester=lambda: True,
            client=client,
            logger=self._logger,
            session_builder=lambda: self._build_session_metadata(
                provider_name=provider.name,
                language=language,
                client=client,
                user_agent=user_agent,
                filename=filename,
                content_type=content_type,
            ),
        )

        capture.begin_capture()
        if capture.state == CaptureState.ERROR:
            reason = buffer.error_reason or "capture_failed"
            raise VoiceServiceError(reason, reason.replace("_", " "))

        try:
            capture.handle_chunk(audio_bytes)
            capture.end_capture("client_stop")
        except Exception as exc:
            raise VoiceServiceError("provider_error", str(exc)) from exc

        if buffer.error_reason:
            if buffer.error_reason in ("unauthorized", "forbidden"):
                provider_cfg = self.config.providers.get(
                    self.config.provider or "openai_whisper", {}
                )
                api_key_env = provider_cfg.get("api_key_env", "OPENAI_API_KEY")
                raise VoicePermanentError(
                    buffer.error_reason,
                    f"OpenAI API key rejected ({buffer.error_reason}); check {api_key_env}",
                    user_message=f"Voice transcription failed: Invalid API key. Please set {api_key_env}.",
                )
            if buffer.error_reason == "invalid_audio":
                meta = ""
                if filename or content_type:
                    meta = f" (file={filename or 'audio'}, type={content_type or 'unknown'})"
                raise VoicePermanentError(
                    "invalid_audio",
                    "OpenAI rejected the audio upload (bad request). "
                    f"Try re-recording or switching formats/browsers{meta}.",
                    user_message="Voice transcription failed: Invalid audio. Try re-recording.",
                )
            if buffer.error_reason == "audio_too_large":
                raise VoicePermanentError(
                    "audio_too_large",
                    "Audio upload too large; record a shorter clip and try again.",
                    user_message="Voice transcription failed: Audio too large. Record a shorter clip.",
                )
            if buffer.error_reason == "rate_limited":
                raise VoiceTransientError(
                    "rate_limited",
                    "OpenAI rate limited the request; wait a moment and try again.",
                    user_message="Voice transcription rate limited. Retrying...",
                )
            if buffer.error_reason == "local_runtime_dependency_missing":
                provider = self.effective_provider_name()
                raise VoicePermanentError(
                    "local_runtime_dependency_missing",
                    "Local voice runtime dependencies are unavailable. Ensure ffmpeg "
                    "is installed and available on PATH.",
                    user_message=(
                        f"Voice transcription failed: local provider '{provider}' "
                        "needs ffmpeg on PATH."
                    ),
                )
            if buffer.error_reason == "local_provider_unavailable":
                provider = self.effective_provider_name()
                provider_spec = local_voice_provider_spec(provider)
                extra = provider_spec[2] if provider_spec is not None else "voice-local"
                raise VoicePermanentError(
                    "local_provider_unavailable",
                    "Local Whisper provider is unavailable. Install optional "
                    "dependencies with: "
                    f"pip install 'codex-autorunner[{extra}]'",
                    user_message=(
                        f"Voice transcription failed: local provider '{provider}' is "
                        f"not installed. Install 'codex-autorunner[{extra}]'."
                    ),
                )
            raise VoiceServiceError(
                buffer.error_reason, buffer.error_reason.replace("_", " ")
            )

        transcript = buffer.final_text or buffer.partial_text or ""
        return {
            "text": transcript,
            "warnings": buffer.warnings,
        }

    async def transcribe_async(
        self,
        audio_bytes: bytes,
        *,
        client: str = "web",
        user_agent: Optional[str] = None,
        language: Optional[str] = None,
        filename: Optional[str] = None,
        content_type: Optional[str] = None,
    ) -> dict:
        async with self._circuit_breaker.call():
            return await asyncio.to_thread(
                self.transcribe,
                audio_bytes,
                client=client,
                user_agent=user_agent,
                language=language,
                filename=filename,
                content_type=content_type,
            )

    def _resolve_provider(self):
        if self._provider is None:
            try:
                self._provider = self._provider_resolver(
                    self.config,
                    logger=self._logger,
                    env=self._env,
                )
            except TypeError:
                try:
                    self._provider = self._provider_resolver(
                        self.config,
                        logger=self._logger,
                    )
                except TypeError:
                    self._provider = self._provider_resolver(self.config)
        return self._provider

    def _configured_provider_name(self) -> str:
        provider_name = normalize_voice_provider(self.config.provider or "")
        return provider_name or "openai_whisper"

    def effective_provider_name(self) -> str:
        configured_provider = self._configured_provider_name()
        provider = self._provider
        if provider is None:
            try:
                provider = self._resolve_provider()
            except ValueError:
                return configured_provider
        provider_name = getattr(provider, "name", None)
        if isinstance(provider_name, str):
            normalized_provider = normalize_voice_provider(provider_name)
            if normalized_provider == "openai_whisper":
                return normalized_provider
            if local_voice_provider_spec(normalized_provider) is not None:
                return normalized_provider
        return configured_provider

    def _provider_resolution_error(self, exc: ValueError) -> VoiceServiceError:
        message = str(exc).strip() or "Voice provider is unavailable"
        provider_name = self._configured_provider_name()
        provider_cfg = self.config.providers.get(provider_name, {})
        api_key_env_name = "OPENAI_API_KEY"
        if isinstance(provider_cfg, Mapping):
            raw_api_key_env = provider_cfg.get("api_key_env")
            if isinstance(raw_api_key_env, str) and raw_api_key_env.strip():
                api_key_env_name = raw_api_key_env.strip()

        if "api key env" in message.lower():
            return VoicePermanentError(
                "missing_api_key",
                message,
                user_message=(
                    "Voice transcription failed: missing API key. "
                    f"Set {api_key_env_name} or install a local whisper provider."
                ),
            )
        return VoicePermanentError(
            "provider_unavailable",
            message,
            user_message=message,
        )

    def _build_session_metadata(
        self,
        *,
        provider_name: str,
        language: Optional[str],
        client: Optional[str],
        user_agent: Optional[str],
        filename: Optional[str] = None,
        content_type: Optional[str] = None,
    ) -> SpeechSessionMetadata:
        return SpeechSessionMetadata(
            session_id=str(uuid.uuid4()),
            provider=provider_name,
            latency_mode=self.config.latency_mode,
            language=language,
            client=client,
            user_agent=user_agent,
            filename=filename,
            content_type=content_type,
        )


class _TranscriptionBuffer:
    def __init__(self) -> None:
        self.partial_text = ""
        self.final_text = ""
        self.warnings: list[str] = []
        self.error_reason: Optional[str] = None
        self.callbacks = CaptureCallbacks(
            on_partial=self._on_partial,
            on_final=self._on_final,
            on_warning=self._on_warning,
            on_error=self._on_error,
        )

    def _on_partial(self, text: str) -> None:
        if text:
            self.partial_text = text

    def _on_final(self, text: str) -> None:
        if text:
            self.final_text = text

    def _on_warning(self, message: str) -> None:
        if message:
            self.warnings.append(message)

    def _on_error(self, reason: str) -> None:
        self.error_reason = reason
