"""
Text-to-Speech Module for NAVIG

Provides multi-provider TTS with voice messaging support,
inspired by comprehensive TTS pipeline patterns.

Supported Providers:
- OpenAI TTS (gpt-4o-mini-tts, tts-1, tts-1-hd)
- ElevenLabs
- Edge TTS (free, no API key required)

Features:
- Provider fallback chain
- Voice-compatible audio for Telegram/WhatsApp
- Configurable voice settings
- Auto-summarization for long text
- Caching for repeated phrases

Usage:
    from navig.voice import speak, TTS
    
    # Simple usage
    audio_path = await speak("Hello world!")
    
    # With provider selection
    tts = TTS(provider="openai")
    result = await tts.synthesize("Hello!", voice="nova")
"""

from __future__ import annotations

import asyncio
import hashlib
import os
import tempfile
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional

from navig.llm_router import PROVIDER_RESOURCE_URLS as _PRUL  # noqa: F401

# =============================================================================
# Types
# =============================================================================

class TTSProvider(str, Enum):
    """Supported TTS providers."""
    OPENAI = "openai"
    ELEVENLABS = "elevenlabs"
    EDGE = "edge"
    GOOGLE_CLOUD = "google_cloud"
    DEEPGRAM = "deepgram"


class TTSVoice(str, Enum):
    """Common voice identifiers."""
    # OpenAI voices
    ALLOY = "alloy"
    ECHO = "echo"
    FABLE = "fable"
    ONYX = "onyx"
    NOVA = "nova"
    SHIMMER = "shimmer"

    # Edge TTS voices (selected)
    EDGE_EN_US_JENNY = "en-US-JennyNeural"
    EDGE_EN_US_GUY = "en-US-GuyNeural"
    EDGE_EN_GB_SONIA = "en-GB-SoniaNeural"
    EDGE_FR_DENISE = "fr-FR-DeniseNeural"
    EDGE_DE_KATJA = "de-DE-KatjaNeural"


@dataclass
class TTSConfig:
    """TTS configuration."""
    # Provider
    provider: TTSProvider = TTSProvider.EDGE
    fallback_providers: List[TTSProvider] = field(
        default_factory=lambda: [TTSProvider.EDGE]
    )

    # Voice settings
    voice: str = "en-US-JennyNeural"
    speed: float = 1.0
    pitch: str = "+0Hz"

    # OpenAI settings
    openai_model: str = "tts-1"
    openai_voice: str = "nova"

    # ElevenLabs settings
    elevenlabs_model: str = "eleven_multilingual_v2"
    elevenlabs_voice_id: str = "pMsXgVXv3BLzUgSXRplE"
    elevenlabs_stability: float = 0.5
    elevenlabs_similarity: float = 0.75

    # Google Cloud TTS settings
    google_cloud_language: str = "en-US"
    google_cloud_voice: str = "en-US-Neural2-C"
    google_cloud_encoding: str = "MP3"
    google_cloud_speaking_rate: float = 1.0

    # Deepgram TTS settings
    deepgram_model: str = "aura-asteria-en"

    # Output
    output_format: str = "mp3"
    sample_rate: int = 24000

    # Limits
    max_text_length: int = 4096
    auto_summarize: bool = True
    summarize_threshold: int = 1500

    # Caching
    cache_enabled: bool = True
    cache_dir: Optional[Path] = None

    # Timeout
    timeout_seconds: int = 30

    def get_cache_dir(self) -> Path:
        """Get cache directory, creating if needed."""
        if self.cache_dir:
            cache = self.cache_dir
        else:
            cache = Path.home() / ".navig" / "cache" / "tts"
        cache.mkdir(parents=True, exist_ok=True)
        return cache


@dataclass
class TTSResult:
    """Result from TTS synthesis."""
    success: bool
    audio_path: Optional[Path] = None
    audio_data: Optional[bytes] = None

    # Metadata
    provider: Optional[TTSProvider] = None
    voice: Optional[str] = None
    duration_ms: Optional[int] = None
    latency_ms: Optional[int] = None

    # Audio properties
    format: str = "mp3"
    sample_rate: int = 24000
    voice_compatible: bool = False  # True if suitable for voice messages

    # Error info
    error: Optional[str] = None

    def __bool__(self) -> bool:
        return self.success


# =============================================================================
# TTS Engine
# =============================================================================

class TTS:
    """
    Text-to-Speech engine with multi-provider support.
    
    Supports OpenAI, ElevenLabs, and Edge TTS with automatic fallback.
    """

    def __init__(self, config: Optional[TTSConfig] = None):
        self.config = config or TTSConfig()
        self._cache: Dict[str, Path] = {}

    # =========================================================================
    # Main API
    # =========================================================================

    async def synthesize(
        self,
        text: str,
        provider: Optional[TTSProvider] = None,
        voice: Optional[str] = None,
        output_path: Optional[Path] = None,
        **kwargs
    ) -> TTSResult:
        """
        Synthesize speech from text.
        
        Args:
            text: Text to convert to speech
            provider: Override provider (uses config default if not specified)
            voice: Override voice
            output_path: Where to save audio (temp file if not specified)
            **kwargs: Additional provider-specific options
            
        Returns:
            TTSResult with audio path or error
        """
        start_time = datetime.utcnow()

        # Validate and prepare text
        text = self._prepare_text(text)
        if not text:
            return TTSResult(success=False, error="Empty text")

        # Check cache
        cache_key = self._get_cache_key(text, provider, voice)
        if self.config.cache_enabled:
            cached = self._get_cached(cache_key)
            if cached:
                return TTSResult(
                    success=True,
                    audio_path=cached,
                    provider=provider or self.config.provider,
                    voice=voice or self.config.voice,
                    format=self.config.output_format,
                )

        # Try providers in order
        providers = [provider] if provider else [self.config.provider] + self.config.fallback_providers

        last_error = None
        for prov in providers:
            if prov is None:
                continue

            try:
                result = await self._synthesize_with_provider(
                    text, prov, voice, output_path, **kwargs
                )
                if result.success:
                    # Calculate latency
                    result.latency_ms = int(
                        (datetime.utcnow() - start_time).total_seconds() * 1000
                    )

                    # Cache on success
                    if self.config.cache_enabled and result.audio_path:
                        self._cache[cache_key] = result.audio_path

                    return result
                else:
                    last_error = result.error
            except Exception as e:
                last_error = str(e)
                continue

        return TTSResult(
            success=False,
            error=f"All providers failed. Last error: {last_error}"
        )

    async def speak(self, text: str, **kwargs) -> Optional[Path]:
        """
        Convenience method - synthesize and return path.
        
        Returns None on failure.
        """
        result = await self.synthesize(text, **kwargs)
        return result.audio_path if result.success else None

    # =========================================================================
    # Provider Implementations
    # =========================================================================

    async def _synthesize_with_provider(
        self,
        text: str,
        provider: TTSProvider,
        voice: Optional[str],
        output_path: Optional[Path],
        **kwargs
    ) -> TTSResult:
        """Dispatch to provider-specific implementation."""
        if provider == TTSProvider.OPENAI:
            return await self._synthesize_openai(text, voice, output_path, **kwargs)
        elif provider == TTSProvider.ELEVENLABS:
            return await self._synthesize_elevenlabs(text, voice, output_path, **kwargs)
        elif provider == TTSProvider.EDGE:
            return await self._synthesize_edge(text, voice, output_path, **kwargs)
        elif provider == TTSProvider.GOOGLE_CLOUD:
            return await self._synthesize_google_cloud(text, voice, output_path, **kwargs)
        elif provider == TTSProvider.DEEPGRAM:
            return await self._synthesize_deepgram(text, voice, output_path, **kwargs)
        else:
            return TTSResult(success=False, error=f"Unknown provider: {provider}")

    async def _synthesize_openai(
        self,
        text: str,
        voice: Optional[str],
        output_path: Optional[Path],
        **kwargs
    ) -> TTSResult:
        """Synthesize using OpenAI TTS API."""
        try:
            import aiohttp
        except ImportError:
            return TTSResult(success=False, error="aiohttp not installed")

        api_key = os.environ.get("OPENAI_API_KEY")
        if not api_key:
            return TTSResult(success=False, error="OPENAI_API_KEY not set")

        voice = voice or self.config.openai_voice
        model = kwargs.get("model", self.config.openai_model)

        url = "https://api.openai.com/v1/audio/speech"
        headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        }
        payload = {
            "model": model,
            "input": text,
            "voice": voice,
            "response_format": "mp3",
            "speed": self.config.speed,
        }

        try:
            async with aiohttp.ClientSession() as session:
                async with session.post(
                    url,
                    headers=headers,
                    json=payload,
                    timeout=aiohttp.ClientTimeout(total=self.config.timeout_seconds)
                ) as response:
                    if response.status != 200:
                        error_text = await response.text()
                        return TTSResult(
                            success=False,
                            error=f"OpenAI API error {response.status}: {error_text}"
                        )

                    audio_data = await response.read()

                    # Save to file
                    if output_path is None:
                        output_path = self._get_temp_path("openai", ".mp3")

                    output_path.write_bytes(audio_data)

                    return TTSResult(
                        success=True,
                        audio_path=output_path,
                        audio_data=audio_data,
                        provider=TTSProvider.OPENAI,
                        voice=voice,
                        format="mp3",
                        sample_rate=24000,
                        voice_compatible=True,  # OpenAI MP3 works for voice messages
                    )

        except asyncio.TimeoutError:
            return TTSResult(success=False, error="OpenAI TTS timeout")
        except Exception as e:
            return TTSResult(success=False, error=f"OpenAI TTS error: {e}")

    async def _synthesize_elevenlabs(
        self,
        text: str,
        voice: Optional[str],
        output_path: Optional[Path],
        **kwargs
    ) -> TTSResult:
        """Synthesize using ElevenLabs API."""
        try:
            import aiohttp
        except ImportError:
            return TTSResult(success=False, error="aiohttp not installed")

        api_key = os.environ.get("ELEVENLABS_API_KEY") or os.environ.get("XI_API_KEY")
        if not api_key:
            return TTSResult(success=False, error="ELEVENLABS_API_KEY not set")

        voice_id = voice or self.config.elevenlabs_voice_id
        model_id = kwargs.get("model_id", self.config.elevenlabs_model)

        url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
        headers = {
            "xi-api-key": api_key,
            "Content-Type": "application/json",
        }
        payload = {
            "text": text,
            "model_id": model_id,
            "voice_settings": {
                "stability": self.config.elevenlabs_stability,
                "similarity_boost": self.config.elevenlabs_similarity,
            }
        }

        try:
            async with aiohttp.ClientSession() as session:
                async with session.post(
                    url,
                    headers=headers,
                    json=payload,
                    timeout=aiohttp.ClientTimeout(total=self.config.timeout_seconds)
                ) as response:
                    if response.status != 200:
                        error_text = await response.text()
                        return TTSResult(
                            success=False,
                            error=f"ElevenLabs API error {response.status}: {error_text}"
                        )

                    audio_data = await response.read()

                    if output_path is None:
                        output_path = self._get_temp_path("elevenlabs", ".mp3")

                    output_path.write_bytes(audio_data)

                    return TTSResult(
                        success=True,
                        audio_path=output_path,
                        audio_data=audio_data,
                        provider=TTSProvider.ELEVENLABS,
                        voice=voice_id,
                        format="mp3",
                        sample_rate=44100,
                        voice_compatible=True,
                    )

        except asyncio.TimeoutError:
            return TTSResult(success=False, error="ElevenLabs TTS timeout")
        except Exception as e:
            return TTSResult(success=False, error=f"ElevenLabs TTS error: {e}")

    async def _synthesize_edge(
        self,
        text: str,
        voice: Optional[str],
        output_path: Optional[Path],
        **kwargs
    ) -> TTSResult:
        """Synthesize using Edge TTS (free, no API key)."""
        try:
            import edge_tts
        except ImportError:
            return TTSResult(
                success=False,
                error="edge-tts not installed. Install with: pip install edge-tts"
            )

        voice = voice or self.config.voice

        if output_path is None:
            output_path = self._get_temp_path("edge", ".mp3")

        try:
            communicate = edge_tts.Communicate(
                text,
                voice,
                rate=f"{int((self.config.speed - 1) * 100):+d}%",
                pitch=self.config.pitch,
            )

            await communicate.save(str(output_path))

            return TTSResult(
                success=True,
                audio_path=output_path,
                provider=TTSProvider.EDGE,
                voice=voice,
                format="mp3",
                sample_rate=24000,
                voice_compatible=True,
            )

        except Exception as e:
            return TTSResult(success=False, error=f"Edge TTS error: {e}")

    async def _synthesize_google_cloud(
        self,
        text: str,
        voice: Optional[str],
        output_path: Optional[Path],
        **kwargs
    ) -> TTSResult:
        """Synthesize using Google Cloud Text-to-Speech API."""
        try:
            import aiohttp
        except ImportError:
            return TTSResult(success=False, error="aiohttp not installed")

        api_key = os.environ.get("GOOGLE_CLOUD_API_KEY") or os.environ.get("GOOGLE_TTS_API_KEY")
        if not api_key:
            return TTSResult(success=False, error="GOOGLE_CLOUD_API_KEY not set")

        voice_name = voice or self.config.google_cloud_voice
        language = kwargs.get("language", self.config.google_cloud_language)

        url = f"https://texttospeech.googleapis.com/v1/text:synthesize?key={api_key}"
        payload = {
            "input": {"text": text},
            "voice": {
                "languageCode": language,
                "name": voice_name,
            },
            "audioConfig": {
                "audioEncoding": self.config.google_cloud_encoding,
                "speakingRate": self.config.google_cloud_speaking_rate,
            },
        }

        try:
            async with aiohttp.ClientSession() as session:
                async with session.post(
                    url,
                    json=payload,
                    timeout=aiohttp.ClientTimeout(total=self.config.timeout_seconds),
                ) as response:
                    if response.status != 200:
                        error_text = await response.text()
                        return TTSResult(
                            success=False,
                            error=f"Google Cloud TTS error {response.status}: {error_text}",
                        )

                    import base64
                    data = await response.json()
                    audio_content = data.get("audioContent", "")
                    audio_data = base64.b64decode(audio_content)

                    if output_path is None:
                        output_path = self._get_temp_path("google", ".mp3")

                    output_path.write_bytes(audio_data)

                    return TTSResult(
                        success=True,
                        audio_path=output_path,
                        audio_data=audio_data,
                        provider=TTSProvider.GOOGLE_CLOUD,
                        voice=voice_name,
                        format="mp3",
                        sample_rate=24000,
                        voice_compatible=True,
                    )
        except asyncio.TimeoutError:
            return TTSResult(success=False, error="Google Cloud TTS timeout")
        except Exception as e:
            return TTSResult(success=False, error=f"Google Cloud TTS error: {e}")

    async def _synthesize_deepgram(
        self,
        text: str,
        voice: Optional[str],
        output_path: Optional[Path],
        **kwargs
    ) -> TTSResult:
        """Synthesize using Deepgram Aura TTS API."""
        try:
            import aiohttp
        except ImportError:
            return TTSResult(success=False, error="aiohttp not installed")

        api_key = os.environ.get("DEEPGRAM_API_KEY")
        if not api_key:
            return TTSResult(success=False, error="DEEPGRAM_API_KEY not set")

        model = voice or self.config.deepgram_model

        url = f"https://api.deepgram.com/v1/speak?model={model}"
        headers = {
            "Authorization": f"Token {api_key}",
            "Content-Type": "application/json",
        }
        payload = {"text": text}

        try:
            async with aiohttp.ClientSession() as session:
                async with session.post(
                    url,
                    headers=headers,
                    json=payload,
                    timeout=aiohttp.ClientTimeout(total=self.config.timeout_seconds),
                ) as response:
                    if response.status != 200:
                        error_text = await response.text()
                        return TTSResult(
                            success=False,
                            error=f"Deepgram TTS error {response.status}: {error_text}",
                        )

                    audio_data = await response.read()

                    if output_path is None:
                        output_path = self._get_temp_path("deepgram", ".mp3")

                    output_path.write_bytes(audio_data)

                    return TTSResult(
                        success=True,
                        audio_path=output_path,
                        audio_data=audio_data,
                        provider=TTSProvider.DEEPGRAM,
                        voice=model,
                        format="mp3",
                        sample_rate=24000,
                        voice_compatible=True,
                    )
        except asyncio.TimeoutError:
            return TTSResult(success=False, error="Deepgram TTS timeout")
        except Exception as e:
            return TTSResult(success=False, error=f"Deepgram TTS error: {e}")

    # =========================================================================
    # Helpers
    # =========================================================================

    def _prepare_text(self, text: str) -> str:
        """Prepare text for synthesis."""
        # Strip whitespace
        text = text.strip()

        # Truncate if too long
        if len(text) > self.config.max_text_length:
            text = text[:self.config.max_text_length] + "..."

        return text

    def _get_cache_key(
        self,
        text: str,
        provider: Optional[TTSProvider],
        voice: Optional[str]
    ) -> str:
        """Generate cache key for text+provider+voice combo."""
        prov = provider or self.config.provider
        v = voice or self.config.voice
        key_data = f"{prov.value}:{v}:{text}"
        return hashlib.sha256(key_data.encode()).hexdigest()[:16]

    def _get_cached(self, key: str) -> Optional[Path]:
        """Check cache for existing audio."""
        if key in self._cache:
            path = self._cache[key]
            if path.exists():
                return path
            else:
                del self._cache[key]

        # Check disk cache
        cache_dir = self.config.get_cache_dir()
        cache_file = cache_dir / f"{key}.mp3"
        if cache_file.exists():
            self._cache[key] = cache_file
            return cache_file

        return None

    def _get_temp_path(self, prefix: str, suffix: str) -> Path:
        """Get a temp file path for audio output."""
        if self.config.cache_enabled:
            cache_dir = self.config.get_cache_dir()
            return cache_dir / f"{prefix}_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}{suffix}"
        else:
            fd, path = tempfile.mkstemp(suffix=suffix, prefix=f"navig_tts_{prefix}_")
            os.close(fd)
            return Path(path)

    # =========================================================================
    # Voice Listing
    # =========================================================================

    async def list_voices(self, provider: Optional[TTSProvider] = None) -> List[Dict[str, str]]:
        """
        List available voices for a provider.
        
        Args:
            provider: Provider to list voices for (uses config default if None)
            
        Returns:
            List of voice dicts with id, name, language
        """
        provider = provider or self.config.provider

        if provider == TTSProvider.OPENAI:
            return [
                {"id": "alloy", "name": "Alloy", "language": "en"},
                {"id": "echo", "name": "Echo", "language": "en"},
                {"id": "fable", "name": "Fable", "language": "en"},
                {"id": "onyx", "name": "Onyx", "language": "en"},
                {"id": "nova", "name": "Nova", "language": "en"},
                {"id": "shimmer", "name": "Shimmer", "language": "en"},
            ]

        elif provider == TTSProvider.EDGE:
            try:
                import edge_tts
                voices = await edge_tts.list_voices()
                return [
                    {
                        "id": v["ShortName"],
                        "name": v["FriendlyName"],
                        "language": v["Locale"],
                    }
                    for v in voices
                ]
            except ImportError:
                return []

        return []


# =============================================================================
# Module-level convenience
# =============================================================================

_default_tts: Optional[TTS] = None


def get_tts() -> TTS:
    """Get default TTS instance."""
    global _default_tts
    if _default_tts is None:
        _default_tts = TTS()
    return _default_tts


async def speak(text: str, **kwargs) -> Optional[Path]:
    """Synthesize speech and return audio path."""
    return await get_tts().speak(text, **kwargs)


async def synthesize(text: str, **kwargs) -> TTSResult:
    """Synthesize speech and return full result."""
    return await get_tts().synthesize(text, **kwargs)


def speak_sync(text: str, **kwargs) -> Optional[Path]:
    """Synchronous wrapper for speak()."""
    return asyncio.run(speak(text, **kwargs))
