"""CosyVoice 语音服务 — Phase 3.

封装阿里云 CosyVoice 语音合成 API，提供：
- 预置音色列表查询
- 音色克隆（提交任务 + 轮询状态）
- 语音合成（提交任务 + 轮询状态）

API 文档: https://help.aliyun.com/zh/model-studio/cosyvoice
"""

from __future__ import annotations

import logging
import time
from dataclasses import dataclass
from typing import Any, Optional

import httpx

from packages.domain.preset_voices import PresetVoice, get_preset_voices
from packages.shared.config import get_shared_settings

logger = logging.getLogger(__name__)


class CosyVoiceError(Exception):
    """CosyVoice API 调用异常。"""

    pass


class CosyVoiceTimeoutError(CosyVoiceError):
    """CosyVoice API 超时。"""

    pass


class CosyVoiceAuthError(CosyVoiceError):
    """CosyVoice API 认证失败。"""

    pass


@dataclass
class CloneResult:
    """音色克隆结果。"""

    voice_id: str
    request_id: str = ""


@dataclass
class SynthesizeResult:
    """语音合成结果。"""

    audio_url: str
    duration: float = 0.0
    file_size: int = 0
    request_id: str = ""


class CosyVoiceService:
    """CosyVoice 语音服务。

    封装阿里云 CosyVoice API，提供音色克隆和语音合成功能。
    支持同步和异步两种模式：
    - 同步：API 直接返回结果
    - 异步：API 返回 task_id，需要轮询状态

    使用示例:
        service = CosyVoiceService(
            api_key="your-api-key",
            base_url="https://dashscope.aliyuncs.com/api/v1/services/aigc/text2audio",
            model="cosyvoice-v1",
        )

        # 获取预置音色
        voices = service.list_preset_voices()

        # 音色克隆
        result = service.clone_voice(audio_url="https://example.com/audio.mp3")

        # 语音合成
        result = service.synthesize_speech(text="你好世界", voice_id="longxiaochun")
    """

    # 轮询配置
    POLL_INTERVAL = 2.0  # 秒
    MAX_POLL_ATTEMPTS = 60  # 最多轮询 60 次（2分钟）

    # 重试配置
    MAX_RETRIES = 3
    RETRY_BACKOFF = 1.0  # 秒，指数退避基数

    def __init__(
        self,
        api_key: str = "",
        base_url: str = "",
        model: str = "",
        http_client: Optional[httpx.Client] = None,
    ) -> None:
        """初始化 CosyVoice 服务。

        Args:
            api_key: CosyVoice API Key，为空时从配置读取
            base_url: CosyVoice API Base URL，为空时从配置读取
            model: CosyVoice 模型名称，为空时从配置读取
            http_client: 可选的 HTTP 客户端（用于测试注入）
        """
        settings = get_shared_settings()

        self._api_key = api_key or settings.cosyvoice_api_key
        self._base_url = base_url or settings.cosyvoice_base_url
        self._model = model or settings.cosyvoice_model

        self._client = http_client or httpx.Client(
            timeout=httpx.Timeout(30.0, connect=10.0),
        )
        self._owns_client = http_client is None

    def __enter__(self) -> CosyVoiceService:
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    def close(self) -> None:
        """关闭 HTTP 客户端。"""
        if self._owns_client and self._client:
            self._client.close()

    # ── 预置音色 ─────────────────────────────────────────

    def list_preset_voices(self) -> list[PresetVoice]:
        """获取预置音色列表。

        Returns:
            预置音色列表
        """
        return get_preset_voices()

    # ── 音色克隆 ─────────────────────────────────────────

    def submit_clone_task(
        self,
        audio_url: str,
        voice_name: str = "",
        language: str = "zh-CN",
    ) -> dict:
        """提交音色克隆任务（非阻塞）。

        只提交任务到 CosyVoice API，不轮询结果。
        返回的 dict 包含 task_id（异步）或 voice_id（同步）。

        Args:
            audio_url: 参考音频 URL
            voice_name: 音色名称（可选）
            language: 语言代码

        Returns:
            dict: {"task_id": str, "voice_id": str, "request_id": str}
                  task_id 和 voice_id 至少有一个非空

        Raises:
            CosyVoiceError: API 调用失败
            CosyVoiceAuthError: 认证失败
            ValueError: 参数无效
        """
        if not audio_url:
            raise ValueError("audio_url 不能为空")
        if not self._api_key:
            raise CosyVoiceAuthError("CosyVoice API Key 未配置")

        payload = {
            "model": self._model,
            "input": {
                "audio_url": audio_url,
            },
            "parameters": {
                "language": language,
            },
        }
        if voice_name:
            payload["parameters"]["voice_name"] = voice_name

        response = self._call_api(
            method="POST",
            path="/services/audio/voice-clone",
            json=payload,
            timeout=60.0,
        )

        output = response.get("output", {})
        task_id = output.get("task_id", "")
        voice_id = output.get("voice_id", "")
        request_id = response.get("request_id", "")

        if not task_id and not voice_id:
            raise CosyVoiceError(f"CosyVoice API 未返回 task_id 或 voice_id: {response}")

        return {
            "task_id": task_id,
            "voice_id": voice_id,
            "request_id": request_id,
        }

    def check_task_status(self, task_id: str) -> dict:
        """查询克隆任务状态（单次查询，不轮询）。

        Args:
            task_id: 任务 ID

        Returns:
            dict: {"status": str, "voice_id": str, "message": str}
                  status 为 SUCCEEDED/FAILED/PENDING/RUNNING

        Raises:
            CosyVoiceError: API 调用失败
            CosyVoiceAuthError: 认证失败
        """
        if not self._api_key:
            raise CosyVoiceAuthError("CosyVoice API Key 未配置")

        response = self._call_api(
            method="GET",
            path=f"/tasks/{task_id}",
            timeout=30.0,
        )

        output = response.get("output", {})
        status = output.get("task_status", "").upper()
        voice_id = output.get("voice_id", "")
        message = output.get("message", "")

        return {
            "status": status,
            "voice_id": voice_id,
            "message": message,
        }

    def poll_clone_task(self, task_id: str, timeout: float = 300.0) -> dict:
        """轮询音色克隆任务状态（公开方法）。

        供 Celery 后台任务调用，轮询直到完成或超时。

        Args:
            task_id: CosyVoice 任务 ID
            timeout: 超时时间（秒），默认 300

        Returns:
            dict: {"voice_id": str}

        Raises:
            CosyVoiceError: 任务失败
            CosyVoiceTimeoutError: 超时
        """
        return self._poll_clone_task(task_id, timeout=timeout)

    def clone_voice(
        self,
        audio_url: str,
        voice_name: str = "",
        language: str = "zh-CN",
        timeout: float = 300.0,
    ) -> CloneResult:
        """克隆音色。

        提交音色克隆任务到 CosyVoice API，并轮询直到完成或超时。

        Args:
            audio_url: 参考音频 URL
            voice_name: 音色名称（可选）
            language: 语言代码
            timeout: 超时时间（秒）

        Returns:
            CloneResult: 克隆结果，包含 voice_id

        Raises:
            CosyVoiceError: API 调用失败
            CosyVoiceTimeoutError: 超时
            CosyVoiceAuthError: 认证失败
            ValueError: 参数无效
        """
        if not audio_url:
            raise ValueError("audio_url 不能为空")
        if not self._api_key:
            raise CosyVoiceAuthError("CosyVoice API Key 未配置")

        # 构建请求
        payload = {
            "model": self._model,
            "input": {
                "audio_url": audio_url,
            },
            "parameters": {
                "language": language,
            },
        }
        if voice_name:
            payload["parameters"]["voice_name"] = voice_name

        # 调用 API
        response = self._call_api(
            method="POST",
            path="/services/audio/voice-clone",
            json=payload,
            timeout=timeout,
        )

        # 解析响应
        output = response.get("output", {})

        # 检查是否有 task_id（异步模式）
        task_id = output.get("task_id")
        voice_id = output.get("voice_id")

        if task_id:
            # 异步模式：轮询任务状态
            result = self._poll_clone_task(task_id, timeout)
            return CloneResult(
                voice_id=result["voice_id"],
                request_id=response.get("request_id", ""),
            )
        elif voice_id:
            # 同步模式：直接返回结果
            return CloneResult(
                voice_id=voice_id,
                request_id=response.get("request_id", ""),
            )
        else:
            raise CosyVoiceError(f"CosyVoice API 未返回 task_id 或 voice_id: {response}")

    def _poll_clone_task(self, task_id: str, timeout: float) -> dict:
        """轮询音色克隆任务状态。

        Args:
            task_id: 任务 ID
            timeout: 超时时间（秒）

        Returns:
            任务结果字典

        Raises:
            CosyVoiceError: 任务失败
            CosyVoiceTimeoutError: 超时
        """
        start_time = time.time()
        attempts = 0

        while attempts < self.MAX_POLL_ATTEMPTS:
            elapsed = time.time() - start_time
            if elapsed > timeout:
                raise CosyVoiceTimeoutError(f"音色克隆任务超时（{timeout}秒）: task_id={task_id}")

            response = self._call_api(
                method="GET",
                path=f"/tasks/{task_id}",
                timeout=30.0,
            )

            output = response.get("output", {})
            status = output.get("task_status", "").upper()

            if status == "SUCCEEDED":
                voice_id = output.get("voice_id", "")
                if not voice_id:
                    raise CosyVoiceError(f"音色克隆任务成功但未返回 voice_id: {response}")
                return {"voice_id": voice_id}
            elif status == "FAILED":
                error_msg = output.get("message", "未知错误")
                raise CosyVoiceError(f"音色克隆任务失败: {error_msg}")
            elif status in ("PENDING", "RUNNING"):
                # 继续轮询
                time.sleep(self.POLL_INTERVAL)
                attempts += 1
            else:
                raise CosyVoiceError(f"未知的任务状态: {status}")

        raise CosyVoiceTimeoutError(f"音色克隆任务轮询次数超限: task_id={task_id}")

    # ── 语音合成 ─────────────────────────────────────────

    def submit_synthesize_task(
        self,
        text: str,
        voice_id: str = "",
        sample_rate: int = 0,
        format: str = "",
        speed: float = 1.0,
    ) -> dict:
        """提交语音合成任务（非阻塞）。

        只提交任务到 CosyVoice API，不轮询结果。
        返回的 dict 包含 task_id（异步）或 audio_url（同步）。

        Args:
            text: 要合成的文本
            voice_id: 音色 ID（预置音色或克隆音色）
            sample_rate: 采样率（Hz），0 表示使用配置默认值
            format: 输出格式（mp3/wav/pcm），空表示使用配置默认值
            speed: 语速（0.5-2.0），1.0 为正常速度

        Returns:
            dict: {"task_id": str, "audio_url": str, "request_id": str}
                  task_id 和 audio_url 至少有一个非空

        Raises:
            CosyVoiceError: API 调用失败
            CosyVoiceAuthError: 认证失败
            ValueError: 参数无效
        """
        if not text:
            raise ValueError("text 不能为空")
        if not voice_id:
            raise ValueError("voice_id 不能为空")
        if not self._api_key:
            raise CosyVoiceAuthError("CosyVoice API Key 未配置")

        settings = get_shared_settings()

        payload = {
            "model": self._model,
            "input": {
                "text": text,
            },
            "parameters": {
                "voice": voice_id,
                "sample_rate": sample_rate or settings.cosyvoice_sample_rate,
                "format": format or settings.cosyvoice_format,
                "rate": speed,
            },
        }

        response = self._call_api(
            method="POST",
            path="/services/aigc/text2audio/generation",
            json=payload,
            timeout=60.0,
        )

        output = response.get("output", {})
        task_id = output.get("task_id", "")
        audio_url = output.get("audio_url", "")
        request_id = response.get("request_id", "")

        if not task_id and not audio_url:
            raise CosyVoiceError(f"CosyVoice API 未返回 task_id 或 audio_url: {response}")

        return {
            "task_id": task_id,
            "audio_url": audio_url,
            "duration": output.get("duration", 0.0),
            "file_size": output.get("file_size", 0),
            "request_id": request_id,
        }

    def poll_synthesize_task(self, task_id: str, timeout: float = 120.0) -> dict:
        """轮询语音合成任务状态（公开方法）。

        供 Celery 后台任务调用，轮询直到完成或超时。

        Args:
            task_id: CosyVoice 任务 ID
            timeout: 超时时间（秒），默认 120

        Returns:
            dict: {"audio_url": str, "duration": float, "file_size": int}

        Raises:
            CosyVoiceError: 任务失败
            CosyVoiceTimeoutError: 超时
        """
        return self._poll_synthesize_task(task_id, timeout=timeout)

    def synthesize_speech(
        self,
        text: str,
        voice_id: str = "",
        sample_rate: int = 0,
        format: str = "",
        speed: float = 1.0,
        timeout: float = 120.0,
    ) -> SynthesizeResult:
        """语音合成。

        提交语音合成任务到 CosyVoice API，并轮询直到完成或超时。

        Args:
            text: 要合成的文本
            voice_id: 音色 ID（预置音色或克隆音色）
            sample_rate: 采样率（Hz），0 表示使用配置默认值
            format: 输出格式（mp3/wav/pcm），空表示使用配置默认值
            speed: 语速（0.5-2.0），1.0 为正常速度
            timeout: 超时时间（秒）

        Returns:
            SynthesizeResult: 合成结果，包含 audio_url

        Raises:
            CosyVoiceError: API 调用失败
            CosyVoiceTimeoutError: 超时
            CosyVoiceAuthError: 认证失败
            ValueError: 参数无效
        """
        if not text:
            raise ValueError("text 不能为空")
        if not voice_id:
            raise ValueError("voice_id 不能为空")
        if not self._api_key:
            raise CosyVoiceAuthError("CosyVoice API Key 未配置")

        settings = get_shared_settings()

        # 构建请求
        payload = {
            "model": self._model,
            "input": {
                "text": text,
            },
            "parameters": {
                "voice": voice_id,
                "sample_rate": sample_rate or settings.cosyvoice_sample_rate,
                "format": format or settings.cosyvoice_format,
                "rate": speed,
            },
        }

        # 调用 API
        response = self._call_api(
            method="POST",
            path="/services/aigc/text2audio/generation",
            json=payload,
            timeout=timeout,
        )

        # 解析响应
        output = response.get("output", {})

        # 检查是否有 task_id（异步模式）
        task_id = output.get("task_id")
        audio_url = output.get("audio_url")

        if task_id:
            # 异步模式：轮询任务状态
            result = self._poll_synthesize_task(task_id, timeout)
            return SynthesizeResult(
                audio_url=result["audio_url"],
                duration=result.get("duration", 0.0),
                file_size=result.get("file_size", 0),
                request_id=response.get("request_id", ""),
            )
        elif audio_url:
            # 同步模式：直接返回结果
            return SynthesizeResult(
                audio_url=audio_url,
                duration=output.get("duration", 0.0),
                file_size=output.get("file_size", 0),
                request_id=response.get("request_id", ""),
            )
        else:
            raise CosyVoiceError(f"CosyVoice API 未返回 audio_url 或 task_id: {response}")

    def _poll_synthesize_task(self, task_id: str, timeout: float) -> dict:
        """轮询语音合成任务状态。

        Args:
            task_id: 任务 ID
            timeout: 超时时间（秒）

        Returns:
            任务结果字典

        Raises:
            CosyVoiceError: 任务失败
            CosyVoiceTimeoutError: 超时
        """
        start_time = time.time()
        attempts = 0

        while attempts < self.MAX_POLL_ATTEMPTS:
            elapsed = time.time() - start_time
            if elapsed > timeout:
                raise CosyVoiceTimeoutError(f"语音合成任务超时（{timeout}秒）: task_id={task_id}")

            response = self._call_api(
                method="GET",
                path=f"/tasks/{task_id}",
                timeout=30.0,
            )

            output = response.get("output", {})
            status = output.get("task_status", "").upper()

            if status == "SUCCEEDED":
                audio_url = output.get("audio_url", "")
                if not audio_url:
                    raise CosyVoiceError(f"语音合成任务成功但未返回 audio_url: {response}")
                return {
                    "audio_url": audio_url,
                    "duration": output.get("duration", 0.0),
                    "file_size": output.get("file_size", 0),
                }
            elif status == "FAILED":
                error_msg = output.get("message", "未知错误")
                raise CosyVoiceError(f"语音合成任务失败: {error_msg}")
            elif status in ("PENDING", "RUNNING"):
                # 继续轮询
                time.sleep(self.POLL_INTERVAL)
                attempts += 1
            else:
                raise CosyVoiceError(f"未知的任务状态: {status}")

        raise CosyVoiceTimeoutError(f"语音合成任务轮询次数超限: task_id={task_id}")

    # ── 内部方法 ─────────────────────────────────────────

    def _call_api(
        self,
        method: str,
        path: str,
        json: Optional[dict] = None,
        timeout: float = 30.0,
    ) -> dict:
        """调用 CosyVoice API。

        支持重试和错误处理。

        Args:
            method: HTTP 方法（GET/POST）
            path: API 路径
            json: 请求体
            timeout: 超时时间（秒）

        Returns:
            API 响应字典

        Raises:
            CosyVoiceError: API 调用失败
            CosyVoiceAuthError: 认证失败
            CosyVoiceTimeoutError: 超时
        """
        url = f"{self._base_url}{path}"
        headers = {
            "Authorization": f"Bearer {self._api_key}",
            "Content-Type": "application/json",
        }

        last_error: Optional[Exception] = None

        for attempt in range(self.MAX_RETRIES):
            try:
                response = self._client.request(
                    method=method,
                    url=url,
                    headers=headers,
                    json=json,
                    timeout=timeout,
                )

                # 处理响应
                if response.status_code == 200:
                    return response.json()
                elif response.status_code in (401, 403):
                    raise CosyVoiceAuthError(f"CosyVoice API 认证失败: HTTP {response.status_code}")
                elif response.status_code >= 500:
                    # 服务端错误，可重试
                    last_error = CosyVoiceError(f"CosyVoice API 服务端错误: HTTP {response.status_code}")
                    logger.warning(
                        f"CosyVoice API 失败 (尝试 {attempt + 1}/{self.MAX_RETRIES}): " f"HTTP {response.status_code}"
                    )
                else:
                    # 客户端错误，不重试
                    raise CosyVoiceError(
                        f"CosyVoice API 调用失败: HTTP {response.status_code}, " f"body={response.text}"
                    )

            except httpx.TimeoutException as e:
                last_error = CosyVoiceTimeoutError(f"请求超时: {e}")
                logger.warning(f"CosyVoice API 超时 (尝试 {attempt + 1}/{self.MAX_RETRIES})")
            except httpx.RequestError as e:
                last_error = CosyVoiceError(f"请求错误: {e}")
                logger.warning(f"CosyVoice API 请求错误 (尝试 {attempt + 1}/{self.MAX_RETRIES}): {e}")

            # 指数退避
            if attempt < self.MAX_RETRIES - 1:
                sleep_time = self.RETRY_BACKOFF * (2**attempt)
                time.sleep(sleep_time)

        # 所有重试都失败
        if last_error:
            raise last_error
        raise CosyVoiceError("CosyVoice API 调用失败，未知错误")
