"""Job 领域模型 — Phase 8 任务 2.10.

统一的异步任务抽象，为视频合成等耗时操作提供异步任务管理能力。

状态机：
  pending → running → success
                  ↘ 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 JobType(StrEnum):
    """任务类型枚举。"""

    VIDEO_COMPOSE = "video_compose"
    """视频合成（VideoComposeService）"""

    RENDER_EDIT_PLAN = "render_edit_plan"
    """剪辑计划渲染"""

    ASSET_INGEST = "asset_ingest"
    """素材导入"""

    CLASSIFICATION = "classification"
    """素材分类"""

    VOICE_EXTRACTION = "voice_extraction"
    """语音提取"""

    GENERATION = "generation"
    """通用生成"""


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

    PENDING = "pending"
    RUNNING = "running"
    SUCCESS = "success"
    FAILED = "failed"
    CANCELLED = "cancelled"


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

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


@dataclass(slots=True)
class Job:
    """统一异步任务实体。

    Attributes:
        id: 任务唯一标识
        project_id: 所属项目
        job_type: 任务类型
        status: 当前状态
        progress: 进度百分比 (0.0 ~ 100.0)
        current_stage: 当前阶段描述（人类可读）
        payload: 任务输入参数（JSON 序列化）
        result: 任务结果（JSON 序列化）
        error_message: 错误信息
        retry_count: 已重试次数
        max_retries: 最大重试次数
        celery_task_id: Celery 异步任务 ID
        source_id: 关联的业务实体 ID（如 edit_plan_id, generation_task_id）
        created_by_user_id: 创建人
        started_at: 开始执行时间
        completed_at: 完成时间
        created_at: 创建时间
        updated_at: 最后更新时间
    """

    id: str
    project_id: str
    job_type: JobType
    status: JobStatus = JobStatus.PENDING
    progress: float = 0.0
    current_stage: str = ""
    payload: dict = field(default_factory=dict)
    result: dict = field(default_factory=dict)
    error_message: str = ""
    retry_count: int = 0
    max_retries: int = 3
    celery_task_id: str = ""
    source_id: str = ""
    created_by_user_id: str = ""
    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,
        project_id: str,
        job_type: JobType | str,
        *,
        payload: dict | None = None,
        source_id: str = "",
        created_by_user_id: str = "",
        max_retries: int = 3,
    ) -> Job:
        """创建新任务。

        Args:
            project_id: 项目 ID
            job_type: 任务类型
            payload: 任务输入参数
            source_id: 关联业务实体 ID
            created_by_user_id: 创建人 ID
            max_retries: 最大重试次数

        Returns:
            新建的 Job 实例

        Raises:
            ValueError: 参数校验失败
        """
        if not project_id.strip():
            raise ValueError("project_id 不能为空")

        # 兼容字符串类型
        if isinstance(job_type, str):
            try:
                job_type = JobType(job_type)
            except ValueError:
                raise ValueError(f"不支持的任务类型: {job_type}")

        return cls(
            id=uuid4().hex,
            project_id=project_id.strip(),
            job_type=job_type,
            payload=payload or {},
            source_id=source_id.strip(),
            created_by_user_id=created_by_user_id.strip(),
            max_retries=max_retries,
        )

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

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

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

        Args:
            new_status: 目标状态

        Raises:
            ValueError: 非法状态转换
        """
        if isinstance(new_status, str):
            try:
                new_status = JobStatus(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

        # 自动设置时间戳
        if new_status == JobStatus.RUNNING and self.started_at is None:
            self.started_at = now
        elif new_status in (JobStatus.SUCCESS, JobStatus.FAILED):
            self.completed_at = now

    def mark_running(self, stage: str = "") -> None:
        """标记为执行中。"""
        self.transition_to(JobStatus.RUNNING)
        if stage:
            self.current_stage = stage

    def mark_success(self, result: dict | None = None) -> None:
        """标记为成功。"""
        self.transition_to(JobStatus.SUCCESS)
        self.progress = 100.0
        self.current_stage = "完成"
        if result:
            self.result = result

    def mark_failed(self, error_message: str) -> None:
        """标记为失败。"""
        self.transition_to(JobStatus.FAILED)
        self.error_message = error_message
        self.current_stage = "失败"

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

    def update_progress(self, progress: float, stage: str = "") -> None:
        """更新进度。

        Args:
            progress: 进度百分比 (0.0 ~ 100.0)
            stage: 当前阶段描述

        Raises:
            ValueError: 进度值非法
        """
        if not 0.0 <= progress <= 100.0:
            raise ValueError(f"进度必须在 0~100 之间，当前: {progress}")
        self.progress = progress
        if stage:
            self.current_stage = stage
        self.updated_at = datetime.now(timezone.utc)

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

        Raises:
            ValueError: 不可重试
        """
        if not self.is_retryable:
            raise ValueError(
                f"任务不可重试: status={self.status.value}, "
                f"retry_count={self.retry_count}, max_retries={self.max_retries}"
            )
        self.retry_count += 1
        self.transition_to(JobStatus.PENDING)
        self.progress = 0.0
        self.current_stage = f"第 {self.retry_count} 次重试"
        self.error_message = ""
        self.started_at = None
        self.completed_at = None
        self.celery_task_id = ""

    def to_dict(self) -> dict:
        """序列化为字典。"""
        return {
            "id": self.id,
            "project_id": self.project_id,
            "job_type": self.job_type.value,
            "status": self.status.value,
            "progress": self.progress,
            "current_stage": self.current_stage,
            "payload": self.payload,
            "result": self.result,
            "error_message": self.error_message,
            "retry_count": self.retry_count,
            "max_retries": self.max_retries,
            "celery_task_id": self.celery_task_id,
            "source_id": self.source_id,
            "created_by_user_id": self.created_by_user_id,
            "is_retryable": self.is_retryable,
            "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,
        }
