"""
Alibaba CosyVoice TTS Adapter

Synthesizes text to speech using Alibaba DashScope CosyVoice API.
Returns 16kHz slin16 PCM chunks for direct injection into DVGateway.

Features:
- CosyVoice v3.5 Plus/Flash models with natural multilingual prosody
- Korean language support via language_hints
- MP3 response → 16kHz PCM conversion via ffmpeg subprocess
- 9 built-in Chinese voices with multilingual capability

Model Reference (2026-03):
  cosyvoice-v3.5-plus  — Highest quality, richer prosody (default)
  cosyvoice-v3.5-flash — Fastest, optimized for real-time voice

Voice options:
  longxiaochun (default), longxiaochun_v2, longyue, longwan,
  longjing, longshuo, longhua, longfei, longshu

API Endpoint: POST https://dashscope.aliyuncs.com/api/v1/services/aigc/text2audio/generation
"""

from __future__ import annotations

import asyncio
import base64
import struct
from typing import AsyncIterable, AsyncIterator

import aiohttp

from dvgateway.audio.codec import float32_to_slin16
from dvgateway.types import (
    TtsAdapter,
    TtsOptions,
    VoiceInfo,
)

DV_SAMPLE_RATE = 16000
CHUNK_SAMPLES = 320  # 20ms at 16kHz
CHUNK_BYTES = CHUNK_SAMPLES * 2  # 640 bytes per 20ms frame

# ── Built-in CosyVoice Voices ────────────────────────────────────────────

COSYVOICE_VOICES: list[VoiceInfo] = [
    VoiceInfo(id="longxiaochun", label="longxiaochun (여성, 기본)"),
    VoiceInfo(id="longxiaochun_v2", label="longxiaochun_v2 (여성, v2)"),
    VoiceInfo(id="longyue", label="longyue (여성)"),
    VoiceInfo(id="longwan", label="longwan (여성)"),
    VoiceInfo(id="longjing", label="longjing (여성)"),
    VoiceInfo(id="longshuo", label="longshuo (남성)"),
    VoiceInfo(id="longhua", label="longhua (남성)"),
    VoiceInfo(id="longfei", label="longfei (남성)"),
    VoiceInfo(id="longshu", label="longshu (남성)"),
]


def _pcm_bytes_to_float32(buf: bytes) -> list[float]:
    """Convert little-endian 16-bit PCM bytes to float32 list."""
    num_samples = len(buf) // 2
    samples = struct.unpack(f"<{num_samples}h", buf[: num_samples * 2])
    return [s / 32768.0 for s in samples]


async def _mp3_to_pcm_16k(mp3_data: bytes) -> bytes:
    """Convert MP3 bytes to 16kHz 16-bit mono PCM via ffmpeg subprocess.

    Args:
        mp3_data: Raw MP3 audio bytes.

    Returns:
        Raw 16-bit little-endian PCM bytes at 16kHz mono.

    Raises:
        RuntimeError: If ffmpeg conversion fails.
    """
    proc = await asyncio.create_subprocess_exec(
        "ffmpeg",
        "-i", "pipe:0",
        "-f", "s16le",
        "-acodec", "pcm_s16le",
        "-ar", "16000",
        "-ac", "1",
        "-loglevel", "error",
        "pipe:1",
        stdin=asyncio.subprocess.PIPE,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout, stderr = await proc.communicate(input=mp3_data)

    if proc.returncode != 0:
        err_msg = stderr.decode("utf-8", errors="replace").strip()
        raise RuntimeError(f"ffmpeg MP3→PCM conversion failed: {err_msg}")

    return stdout


class CosyVoiceAdapter(TtsAdapter):
    """Alibaba CosyVoice TTS adapter implementing the TtsAdapter interface.

    Uses DashScope CosyVoice API for high-quality multilingual speech
    synthesis. Response audio (MP3 or base64) is converted to 16kHz
    slin16 PCM via ffmpeg for DVGateway compatibility.

    Args:
        api_key: DashScope API key (Bearer token).
        voice: Voice name (default: longxiaochun).
        model: CosyVoice model ID (default: cosyvoice-v3.5-plus).
        language_hints: Language hints for pronunciation (default: ["ko"]).
    """

    def __init__(
        self,
        api_key: str = "",
        voice: str = "longxiaochun",
        model: str = "cosyvoice-v3.5-plus",
        language_hints: list[str] | None = None,
    ) -> None:
        self._api_key = api_key
        self._voice = voice
        self._model = model
        self._language_hints = language_hints if language_hints is not None else ["ko"]

    async def _synthesize_impl(self, text: str, options: TtsOptions | None = None) -> AsyncIterator[bytes]:
        """Synthesize text and yield 16kHz slin16 PCM chunks.

        Sends a request to DashScope CosyVoice API, receives audio
        response (JSON with base64 or binary MP3), converts to 16kHz
        PCM via ffmpeg, and yields 20ms PCM frames.
        """
        voice = (options.voice_id if options and options.voice_id else None) or self._voice

        url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text2audio/generation"

        request_body: dict[str, object] = {
            "model": self._model,
            "input": {
                "text": text,
            },
            "parameters": {
                "voice": voice,
            },
        }

        # Add language hints if specified
        if self._language_hints:
            params = request_body["parameters"]
            assert isinstance(params, dict)
            params["language_hints"] = self._language_hints

        headers = {
            "Authorization": f"Bearer {self._api_key}",
            "Content-Type": "application/json",
        }

        async with aiohttp.ClientSession() as session:
            async with session.post(
                url,
                headers=headers,
                json=request_body,
            ) as resp:
                if resp.status != 200:
                    err_body = await resp.text()
                    raise RuntimeError(f"CosyVoice TTS failed (HTTP {resp.status}): {err_body}")

                content_type = resp.content_type or ""

                if "application/json" in content_type:
                    # JSON response with base64 audio
                    data = await resp.json()

                    # Check for API error in response
                    if data.get("code") and data["code"] != "200":
                        raise RuntimeError(
                            f"CosyVoice TTS API error: {data.get('code')} - {data.get('message', '')}"
                        )

                    # Extract audio from output.audio field (base64)
                    output = data.get("output", {})
                    audio_b64 = output.get("audio", "")
                    if not audio_b64:
                        raise RuntimeError("CosyVoice TTS response missing audio content")

                    mp3_data = base64.b64decode(audio_b64)
                else:
                    # Binary MP3 response
                    mp3_data = await resp.read()

        if not mp3_data:
            raise RuntimeError("CosyVoice TTS returned empty audio data")

        # Convert MP3 → 16kHz 16-bit PCM via ffmpeg
        pcm_data = await _mp3_to_pcm_16k(mp3_data)

        # Yield 20ms PCM frames (640 bytes = 320 samples at 16kHz)
        offset = 0
        while offset < len(pcm_data):
            end = min(offset + CHUNK_BYTES, len(pcm_data))
            frame = pcm_data[offset:end]
            offset = end

            # Frame is already 16kHz 16-bit PCM — convert to float32 then back
            # to slin16 with clamping for consistency with other adapters
            samples = _pcm_bytes_to_float32(frame)
            yield float32_to_slin16(samples)

    def synthesize(self, text: str, options: TtsOptions | None = None) -> AsyncIterable[bytes]:
        return self._synthesize_impl(text, options)
