"""TTSJob 领域模型 — Phase 3 CosyVoice 集成.

TTS 任务，用于管理文本转语音的合成请求。

状态机：
  pending → processing → completed
                  ↘ failed → pending (重试)
       ↘ cancelled
"""

from __future__ import annotations

import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone

if sys.version_info >= (3, 11):
    from enum import StrEnum
else:
    from enum import Enum

    class StrEnum(str, Enum):
        pass


from uuid import uuid4


class TTSJobStatus(StrEnum):
    """TTS 任务状态枚举。"""

    PENDING = "pending"
    """待处理（任务已创建，等待执行）"""

    PROCESSING = "processing"
    """处理中（正在调用 CosyVoice API 合成）"""

    COMPLETED = "completed"
    """已完成（音频合成成功）"""

    FAILED = "failed"
    """失败（合成失败）"""

    CANCELLED = "cancelled"
    """已取消（用户取消或系统取消）"""


# 终态集合
TERMINAL_STATUSES = frozenset({TTSJobStatus.COMPLETED, TTSJobStatus.FAILED, TTSJobStatus.CANCELLED})

# 合法状态转换
_VALID_TRANSITIONS: dict[TTSJobStatus, set[TTSJobStatus]] = {
    TTSJobStatus.PENDING: {TTSJobStatus.PROCESSING, TTSJobStatus.FAILED, TTSJobStatus.CANCELLED},
    TTSJobStatus.PROCESSING: {TTSJobStatus.COMPLETED, TTSJobStatus.FAILED, TTSJobStatus.CANCELLED},
    TTSJobStatus.FAILED: {TTSJobStatus.PENDING},  # 重试回到 pending
}


@dataclass(slots=True)
class TTSJob:
    """TTS 任务实体。

    Attributes:
        id: 任务唯一标识
        user_id: 所属用户
        project_id: 所属项目（可选）
        voice_clone_profile_id: 关联的音色克隆档案 ID（可选，使用自定义音色时必填）
        status: 当前状态
        input_text: 输入文本
        voice_id: 使用的音色 ID（CosyVoice 内置音色或克隆音色 ID）
        voice_model: 使用的模型名称
        output_audio_url: 输出音频文件 URL
        output_audio_key: 输出音频 OSS key
        duration: 音频时长（秒）
        file_size: 文件大小（字节）
        sample_rate: 采样率
        format: 输出格式（mp3/wav/pcm）
        error_message: 错误信息
        retry_count: 已重试次数
        max_retries: 最大重试次数
        metadata: 扩展元数据（JSON）
        started_at: 开始处理时间
        completed_at: 完成时间
        created_at: 创建时间
        updated_at: 最后更新时间
    """

    id: str
    user_id: str
    input_text: str
    voice_id: str = ""
    voice_model: str = ""
    project_id: str = ""
    voice_clone_profile_id: str = ""
    status: TTSJobStatus = TTSJobStatus.PENDING
    output_audio_url: str = ""
    output_audio_key: str = ""
    duration: float = 0.0
    file_size: int = 0
    sample_rate: int = 22050
    format: str = "mp3"
    error_message: str = ""
    retry_count: int = 0
    max_retries: int = 3
    metadata: dict = field(default_factory=dict)
    started_at: datetime | None = None
    completed_at: datetime | None = None
    created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
    updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    @classmethod
    def create(
        cls,
        user_id: str,
        input_text: str,
        *,
        voice_id: str = "",
        voice_model: str = "",
        project_id: str = "",
        voice_clone_profile_id: str = "",
        sample_rate: int = 22050,
        format: str = "mp3",
        max_retries: int = 3,
        metadata: dict | None = None,
    ) -> TTSJob:
        """创建 TTS 任务。

        Args:
            user_id: 用户 ID
            input_text: 输入文本
            voice_id: 使用的音色 ID
            voice_model: 使用的模型名称
            project_id: 项目 ID
            voice_clone_profile_id: 关联的音色克隆档案 ID
            sample_rate: 采样率
            format: 输出格式
            max_retries: 最大重试次数
            metadata: 扩展元数据

        Returns:
            新建的 TTSJob 实例

        Raises:
            ValueError: 参数校验失败
        """
        if not user_id.strip():
            raise ValueError("user_id 不能为空")
        if not input_text.strip():
            raise ValueError("input_text 不能为空")
        if len(input_text.strip()) > 10000:
            raise ValueError("input_text 长度不能超过 10000 字符")
        if format not in ("mp3", "wav", "pcm"):
            raise ValueError(f"不支持的输出格式: {format}，支持: mp3/wav/pcm")

        return cls(
            id=uuid4().hex,
            user_id=user_id.strip(),
            input_text=input_text.strip(),
            voice_id=voice_id.strip(),
            voice_model=voice_model.strip(),
            project_id=project_id.strip(),
            voice_clone_profile_id=voice_clone_profile_id.strip(),
            sample_rate=sample_rate,
            format=format.strip(),
            max_retries=max_retries,
            metadata=metadata or {},
        )

    @property
    def is_terminal(self) -> bool:
        """是否处于终态。"""
        return self.status in TERMINAL_STATUSES

    @property
    def is_retryable(self) -> bool:
        """是否可重试（失败且未超过重试上限）。"""
        return self.status == TTSJobStatus.FAILED and self.retry_count < self.max_retries

    @property
    def is_completed(self) -> bool:
        """是否已完成。"""
        return self.status == TTSJobStatus.COMPLETED and bool(self.output_audio_url)

    def transition_to(self, new_status: TTSJobStatus | str) -> None:
        """执行状态转换。

        Args:
            new_status: 目标状态

        Raises:
            ValueError: 非法状态转换
        """
        if isinstance(new_status, str):
            try:
                new_status = TTSJobStatus(new_status)
            except ValueError:
                raise ValueError(f"无效状态: {new_status}")

        allowed = _VALID_TRANSITIONS.get(self.status, set())
        if new_status not in allowed:
            raise ValueError(
                f"非法状态转换: {self.status.value} → {new_status.value}，"
                f"允许: {{{', '.join(s.value for s in allowed)}}}"
            )

        now = datetime.now(timezone.utc)
        self.status = new_status
        self.updated_at = now

    def mark_processing(self) -> None:
        """标记为处理中。"""
        self.transition_to(TTSJobStatus.PROCESSING)
        self.started_at = datetime.now(timezone.utc)
        self.error_message = ""

    def mark_completed(
        self,
        output_audio_url: str,
        *,
        output_audio_key: str = "",
        duration: float = 0.0,
        file_size: int = 0,
    ) -> None:
        """标记为已完成。

        Args:
            output_audio_url: 输出音频 URL
            output_audio_key: 输出音频 OSS key
            duration: 音频时长
            file_size: 文件大小
        """
        if not output_audio_url.strip():
            raise ValueError("output_audio_url 不能为空")
        self.transition_to(TTSJobStatus.COMPLETED)
        self.output_audio_url = output_audio_url.strip()
        self.output_audio_key = output_audio_key.strip()
        self.duration = duration
        self.file_size = file_size
        self.completed_at = datetime.now(timezone.utc)
        self.error_message = ""

    def mark_failed(self, error_message: str) -> None:
        """标记为失败。

        Args:
            error_message: 错误信息
        """
        self.transition_to(TTSJobStatus.FAILED)
        self.error_message = error_message

    def mark_cancelled(self) -> None:
        """标记为已取消。"""
        self.transition_to(TTSJobStatus.CANCELLED)

    def prepare_retry(self) -> None:
        """准备重试：重置状态为 pending。

        Raises:
            ValueError: 不可重试
        """
        if not self.is_retryable:
            raise ValueError(
                f"TTS 任务不可重试: status={self.status.value}, "
                f"retry_count={self.retry_count}, max_retries={self.max_retries}"
            )
        self.retry_count += 1
        self.transition_to(TTSJobStatus.PENDING)
        self.error_message = ""
        self.started_at = None
        self.completed_at = None

    def to_dict(self) -> dict:
        """序列化为字典。"""
        return {
            "id": self.id,
            "user_id": self.user_id,
            "project_id": self.project_id,
            "voice_clone_profile_id": self.voice_clone_profile_id,
            "status": self.status.value,
            "input_text": self.input_text,
            "voice_id": self.voice_id,
            "voice_model": self.voice_model,
            "output_audio_url": self.output_audio_url,
            "output_audio_key": self.output_audio_key,
            "duration": self.duration,
            "file_size": self.file_size,
            "sample_rate": self.sample_rate,
            "format": self.format,
            "error_message": self.error_message,
            "retry_count": self.retry_count,
            "max_retries": self.max_retries,
            "is_retryable": self.is_retryable,
            "is_completed": self.is_completed,
            "metadata": self.metadata,
            "started_at": self.started_at.isoformat() if self.started_at else None,
            "completed_at": self.completed_at.isoformat() if self.completed_at else None,
            "created_at": self.created_at.isoformat() if self.created_at else None,
            "updated_at": self.updated_at.isoformat() if self.updated_at else None,
        }
