# app/providers/tts.py
from __future__ import annotations

import os
import math
import struct
from math import ceil
from typing import TYPE_CHECKING, Any, Generator, List, Optional, Tuple

from app.common.timed import timed

if TYPE_CHECKING:
    from elevenlabs import VoiceSettings

SAMPLE_RATE = 16_000
BYTES_PER_SAMPLE = 2  # PCM16
CHANNELS = 1


def frame_bytes_for_ms(
    ms: int,
    sr: int = SAMPLE_RATE,
    ch: int = CHANNELS,
    bps: int = BYTES_PER_SAMPLE,
) -> int:
    if ms <= 0:
        raise ValueError("frame_ms must be > 0")
    samples_per_channel = round(sr * (ms / 1000.0))
    return int(samples_per_channel * ch * bps)


def _iter_pcm_frames(
    pcm: bytes,
    *,
    frame_ms: int,
    strict_frame_size: bool,
) -> Generator[bytes, None, None]:
    if not pcm:
        return
        yield  # pragma: no cover

    if frame_ms % 10 != 0:
        raise ValueError("frame_ms should be a multiple of 10ms for telephony pipelines")

    frame_bytes = frame_bytes_for_ms(frame_ms)
    offset = 0
    pcm_len = len(pcm)
    while offset + frame_bytes <= pcm_len:
        yield pcm[offset:offset + frame_bytes]
        offset += frame_bytes

    if offset >= pcm_len:
        return

    tail = pcm[offset:]
    if strict_frame_size:
        pad = frame_bytes - len(tail)
        if pad > 0:
            tail = tail + (b"\x00" * pad)
        else:
            tail = tail[:frame_bytes]
    yield tail


def _frames_with_optional_silence(
    *,
    pcm_frames: Generator[bytes, None, None],
    frame_ms: int,
    pre_silence_ms: int,
    post_silence_ms: int,
) -> List[bytes]:
    frame_bytes = frame_bytes_for_ms(frame_ms)
    silence_frame = b"\x00" * frame_bytes
    frames: List[bytes] = []

    if pre_silence_ms > 0:
        frames.extend([silence_frame] * ceil(pre_silence_ms / frame_ms))

    for frame in pcm_frames:
        if len(frame) != frame_bytes:
            if len(frame) < frame_bytes:
                frame = frame + (b"\x00" * (frame_bytes - len(frame)))
            else:
                frame = frame[:frame_bytes]
        frames.append(frame)

    if post_silence_ms > 0:
        frames.extend([silence_frame] * ceil(post_silence_ms / frame_ms))

    return frames


class TTS:
    """
    ElevenLabs TTS -> PCM16 16kHz (mono).
    """

    def __init__(
        self,
        api_key: str,
        voice_id: str,
        model_id: str,
        *,
        voice_settings: Optional["VoiceSettings"] = None,
    ) -> None:
        if not api_key:
            raise ValueError("api_key is required")
        if not voice_id:
            raise ValueError("voice_id is required")

        elevenlabs_sdk = _load_elevenlabs_sdk()
        self.client = elevenlabs_sdk["client_cls"](api_key=api_key)
        self.voice_id = voice_id
        self.model_id = model_id
        self.voice_settings = voice_settings or elevenlabs_sdk["voice_settings_cls"](
            stability=0.45,
            speed=1.02,
            similarity_boost=0.75,
            use_speaker_boost=True,
            style=0.7,
        )

    @timed("TTS")
    def synthesize_pcm16k(self, text: str) -> bytes:
        text = (text or "").strip()
        if not text:
            return b""

        stream = self.client.text_to_speech.convert(
            voice_id=self.voice_id,
            output_format="pcm_16000",
            text=text,
            model_id=self.model_id,
            voice_settings=self.voice_settings,
        )

        out = bytearray()
        for chunk in stream:
            if chunk:
                out.extend(chunk)
        return bytes(out)

    @timed("TTS")
    def synthesize_frames(
        self,
        text: str,
        frame_ms: int = 20,
        *,
        strict_frame_size: bool = True,
    ) -> Generator[bytes, None, None]:
        yield from _iter_pcm_frames(
            self.synthesize_pcm16k(text),
            frame_ms=frame_ms,
            strict_frame_size=strict_frame_size,
        )

    @timed("Intro TTS")
    def synthesize_ws_frames(
        self,
        text: str,
        *,
        frame_ms: int = 20,
        pre_silence_ms: int = 0,
        post_silence_ms: int = 0,
    ) -> List[bytes]:
        text = (text or "").strip()
        if not text:
            return []

        return _frames_with_optional_silence(
            pcm_frames=self.synthesize_frames(text, frame_ms=frame_ms, strict_frame_size=True),
            frame_ms=frame_ms,
            pre_silence_ms=pre_silence_ms,
            post_silence_ms=post_silence_ms,
        )


class FakeTTS:
    """
    Provider local para smoke tests.

    Gera um tom PCM16 simples com duracao proporcional ao texto para validar o
    pipeline sem depender de servicos externos de TTS.
    """

    def __init__(
        self,
        *,
        tone_hz: int = 440,
        amplitude: float = 0.12,
        min_duration_ms: int = 320,
        max_duration_ms: int = 2200,
        char_duration_ms: int = 35,
    ) -> None:
        self.tone_hz = max(120, int(tone_hz))
        self.amplitude = min(max(float(amplitude), 0.0), 0.95)
        self.min_duration_ms = max(80, int(min_duration_ms))
        self.max_duration_ms = max(self.min_duration_ms, int(max_duration_ms))
        self.char_duration_ms = max(5, int(char_duration_ms))

    def _duration_ms_for_text(self, text: str) -> int:
        text = (text or "").strip()
        if not text:
            return 0

        estimated = max(self.min_duration_ms, len(text) * self.char_duration_ms)
        return min(estimated, self.max_duration_ms)

    def _envelope(self, index: int, total_samples: int) -> float:
        fade_samples = max(1, int(SAMPLE_RATE * 0.02))
        if index < fade_samples:
            return index / fade_samples
        tail_start = max(0, total_samples - fade_samples)
        if index >= tail_start:
            return max(0.0, (total_samples - index) / fade_samples)
        return 1.0

    @timed("TTS")
    def synthesize_pcm16k(self, text: str) -> bytes:
        duration_ms = self._duration_ms_for_text(text)
        if duration_ms <= 0:
            return b""

        total_samples = max(1, int(SAMPLE_RATE * duration_ms / 1000))
        out = bytearray()
        angular = (2.0 * math.pi * self.tone_hz) / SAMPLE_RATE

        for sample_index in range(total_samples):
            env = self._envelope(sample_index, total_samples)
            value = math.sin(sample_index * angular) * self.amplitude * env
            out.extend(struct.pack("<h", int(value * 32767)))

        return bytes(out)

    @timed("TTS")
    def synthesize_frames(
        self,
        text: str,
        frame_ms: int = 20,
        *,
        strict_frame_size: bool = True,
    ) -> Generator[bytes, None, None]:
        yield from _iter_pcm_frames(
            self.synthesize_pcm16k(text),
            frame_ms=frame_ms,
            strict_frame_size=strict_frame_size,
        )

    @timed("Intro TTS")
    def synthesize_ws_frames(
        self,
        text: str,
        *,
        frame_ms: int = 20,
        pre_silence_ms: int = 0,
        post_silence_ms: int = 0,
    ) -> List[bytes]:
        text = (text or "").strip()
        if not text:
            return []

        return _frames_with_optional_silence(
            pcm_frames=self.synthesize_frames(text, frame_ms=frame_ms, strict_frame_size=True),
            frame_ms=frame_ms,
            pre_silence_ms=pre_silence_ms,
            post_silence_ms=post_silence_ms,
        )


def build_tts_provider_from_env(
    provider: str,
    *,
    voice_id: str = "",
    model_id: str = "",
    language: str = "",
) -> Tuple[object | None, str | None]:
    normalized = (provider or "elevenlabs").strip().lower()

    if normalized == "fake":
        return (
            FakeTTS(
                tone_hz=int(os.getenv("FAKE_TTS_TONE_HZ", "440") or "440"),
                amplitude=float(os.getenv("FAKE_TTS_AMPLITUDE", "0.12") or "0.12"),
                min_duration_ms=int(os.getenv("FAKE_TTS_MIN_DURATION_MS", "320") or "320"),
                max_duration_ms=int(os.getenv("FAKE_TTS_MAX_DURATION_MS", "2200") or "2200"),
                char_duration_ms=int(os.getenv("FAKE_TTS_CHAR_DURATION_MS", "35") or "35"),
            ),
            None,
        )

    if normalized in {"", "elevenlabs"}:
        api_key = (os.getenv("ELEVENLABS_API_KEY", "") or "").strip()
        resolved_voice_id = (voice_id or os.getenv("ELEVENLABS_VOICE_ID", "")).strip()
        resolved_model_id = (model_id or os.getenv("ELEVENLABS_MODEL_ID", "eleven_flash_v2_5")).strip()
        if not api_key or not resolved_voice_id:
            return None, "missing_elevenlabs_config"
        if not _is_elevenlabs_available():
            return None, "missing_elevenlabs_sdk"
        return TTS(api_key=api_key, voice_id=resolved_voice_id, model_id=resolved_model_id), None

    return None, f"unsupported_tts_provider:{normalized}"


def _load_elevenlabs_sdk() -> dict[str, Any]:
    try:
        from elevenlabs import VoiceSettings
        from elevenlabs.client import ElevenLabs
    except ModuleNotFoundError as exc:
        raise RuntimeError(
            "ElevenLabs SDK is not installed. Install dependency `elevenlabs` to enable this provider."
        ) from exc

    return {
        "voice_settings_cls": VoiceSettings,
        "client_cls": ElevenLabs,
    }


def _is_elevenlabs_available() -> bool:
    try:
        _load_elevenlabs_sdk()
    except RuntimeError:
        return False
    return True
