"""剪辑计划管理 API — Phase 8 模板编排引擎.

RESTful CRUD for EditPlan:
- GET    /api/v1/edit-plans            列表（分页 + 状态/模板筛选）
- GET    /api/v1/edit-plans/{id}       详情
- POST   /api/v1/edit-plans            创建
- PUT    /api/v1/edit-plans/{id}       更新（含状态机流转）
- DELETE /api/v1/edit-plans/{id}       删除
- POST   /api/v1/edit-plans/{id}/generate          触发剪辑渲染生成（任务 2.05）
- GET    /api/v1/edit-plans/{id}/generation-status  查询生成进度（任务 2.05）
- POST   /api/v1/edit-plans/{id}/ai-recommend       AI 推荐片段方案（任务 3.09）
- POST   /api/v1/edit-plans/{id}/generate-cover     AI 生成封面（任务 3.09）

业务逻辑委托给 EditPlanService 服务层。
"""

from __future__ import annotations

import logging
from datetime import datetime
from typing import Any, List, Optional

from app.auth import AuthenticatedUser, get_current_user
from app.core.celery_app import celery_app
from app.dependencies import get_db_session, get_project_repository
from app.schemas.generation_task import GenerationTaskResponse
from app.services import EditPlanService
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session

from packages.adapters.sqlalchemy_impl.generation_task_repository import (
    SQLAlchemyGenerationTaskRepository,
)
from packages.application.generation_tasks import (
    CreateGenerationTaskCommand,
    CreateGenerationTaskUseCase,
)
from packages.domain.config_schemas import normalize_plan_config
from packages.domain.edit_plan import EditPlan, EditPlanStatus

logger = logging.getLogger(__name__)

router = APIRouter()


# ── Pydantic Schemas ─────────────────────────────────────────────────────────


class EditPlanCreateRequest(BaseModel):
    """创建剪辑计划请求体"""

    template_id: str = Field(..., min_length=1, max_length=32, description="关联模板 ID")
    name: str = Field(..., min_length=1, max_length=200, description="计划名称")
    config: dict[str, Any] = Field(default_factory=dict, description="计划配置 (JSON)")
    total_duration: float = Field(default=0.0, ge=0.0, description="总时长 (秒)")
    project_id: str = Field(default="", description="所属项目 ID")


class EditPlanUpdateRequest(BaseModel):
    """更新剪辑计划请求体"""

    name: Optional[str] = Field(default=None, min_length=1, max_length=200, description="计划名称")
    config: Optional[dict[str, Any]] = Field(default=None, description="计划配置 (JSON)")
    total_duration: Optional[float] = Field(default=None, ge=0.0, description="总时长 (秒)")
    status: Optional[str] = Field(
        default=None,
        description="目标状态 (通过状态机流转): editing / rendering / completed / failed / draft",
    )


class EditPlanResponse(BaseModel):
    """剪辑计划响应体"""

    id: str
    template_id: str
    name: str
    status: str
    total_duration: float
    project_id: str = ""
    created_by_user_id: str = ""
    config: dict[str, Any]
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}


class EditPlanListResponse(BaseModel):
    """剪辑计划列表响应体"""

    items: List[EditPlanResponse]
    total: int
    page: int
    page_size: int


class ClipStatusItem(BaseModel):
    """片段生成状态"""

    clip_id: str
    clip_type: str
    order: int
    status: str
    asset_id: str
    text_content: str
    duration: float


class EditPlanGenerationStatusResponse(BaseModel):
    """剪辑计划生成进度响应体"""

    plan_id: str
    plan_status: str
    generation_task_id: Optional[str] = None
    clips: List[ClipStatusItem]


class EditPlanGenerateResponse(BaseModel):
    """剪辑计划触发生成响应体"""

    plan_id: str
    plan_status: str
    generation_task_id: str
    clip_count: int


class EditPlanGenerationsResponse(BaseModel):
    """剪辑计划关联的生成记录列表响应体"""

    items: List[GenerationTaskResponse]
    total: int


# ── AI 推荐片段方案 Schemas（任务 3.09） ──────────────────────────────────────


class AIRecommendRequest(BaseModel):
    """AI 推荐片段方案请求体"""

    asset_ids: List[str] = Field(default_factory=list, description="素材 ID 列表")
    editing_mode: str = Field(default="one_take", description="剪辑模式: one_take / pip / voice_over / voice_pip")
    target_duration: float = Field(default=30.0, ge=1.0, le=600.0, description="目标时长（秒）")


class AIRecommendClipItem(BaseModel):
    """AI 推荐的单个片段"""

    clip_type: str = Field(..., description="片段类型: intro / showcase / title / subtitle / cta / outro")
    order: int = Field(..., ge=0, description="片段顺序")
    text_content: str = Field(default="", description="文字内容")
    duration: float = Field(..., ge=0.0, description="片段时长（秒）")
    transition_effect: str = Field(default="cut", description="转场效果")
    asset_id: str = Field(default="", description="关联素材 ID")
    start_time: float = Field(default=0.0, ge=0.0, description="素材截取起始时间（秒）")
    config: dict[str, Any] = Field(default_factory=dict, description="片段额外配置")


class AIRecommendResponse(BaseModel):
    """AI 推荐片段方案响应体"""

    plan_id: str = Field(..., description="剪辑计划 ID")
    clips: List[AIRecommendClipItem] = Field(..., description="推荐的片段列表")
    config: dict[str, Any] = Field(..., description="推荐的 plan config（cover/title/subtitle/bgm）")
    total_duration: float = Field(..., ge=0.0, description="推荐方案总时长（秒）")
    confidence: float = Field(..., ge=0.0, le=1.0, description="AI 推荐置信度 (0~1)")


# ── AI 封面生成 Schemas（任务 3.09） ─────────────────────────────────────────


class GenerateCoverRequest(BaseModel):
    """AI 封面生成请求体"""

    asset_ids: List[str] = Field(default_factory=list, description="素材 ID 列表（确定视频来源）")
    cover_type: str = Field(
        default="ai_frame",
        description="封面类型: ai_frame / manual / upload / ai_regenerate",
    )
    frame_time: Optional[float] = Field(
        default=None,
        ge=0.0,
        description="手动选帧时间点（秒），仅 cover_type=manual 时有效",
    )


class GenerateCoverResponse(BaseModel):
    """AI 封面生成响应体"""

    plan_id: str = Field(..., description="剪辑计划 ID")
    cover: dict[str, Any] = Field(..., description="封面数据（type / image_url / frame_time 等）")


# ── Helpers ───────────────────────────────────────────────────────────────────


def _check_project_access(project_id: str, user_id: str, project_repository: Any) -> None:
    """校验用户对项目的访问权限（参照 assets.py 的 can_access 模式）"""
    if not project_id or not project_id.strip():
        return
    project = project_repository.find_by_id(project_id)
    if project is None:
        raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
    if not project.can_access(user_id):
        raise HTTPException(status_code=403, detail="Access denied to project")


def _to_response(p: EditPlan) -> EditPlanResponse:
    return EditPlanResponse(
        id=p.id,
        template_id=p.template_id,
        name=p.name,
        status=p.status.value if hasattr(p.status, "value") else p.status,
        total_duration=p.total_duration,
        project_id=p.project_id or "",
        created_by_user_id=p.created_by_user_id or "",
        config=p.config,
        created_at=p.created_at,
        updated_at=p.updated_at,
    )


# ── Routes ────────────────────────────────────────────────────────────────────


@router.get("", response_model=EditPlanListResponse)
def list_plans(
    page: int = Query(default=1, ge=1, description="页码"),
    page_size: int = Query(default=20, ge=1, le=100, description="每页数量"),
    template_id: Optional[str] = Query(default=None, description="按模板 ID 筛选"),
    project_id: Optional[str] = Query(default=None, description="按项目 ID 筛选"),
    status_filter: Optional[str] = Query(
        default=None,
        alias="status",
        description="按状态筛选: draft / editing / rendering / completed / failed",
    ),
    db: Session = Depends(get_db_session),
    current_user: AuthenticatedUser = Depends(get_current_user),
    project_repository: Any = Depends(get_project_repository),
) -> EditPlanListResponse:
    """获取剪辑计划列表（支持分页、按模板/状态/项目筛选）"""
    svc = EditPlanService(db)

    # 空串 project_id 视为未传（避免 DB 中匹配到空串记录）
    if project_id is not None and not project_id.strip():
        project_id = None

    # 解析状态筛选
    status_enum: Optional[EditPlanStatus] = None
    if status_filter:
        try:
            status_enum = EditPlanStatus(status_filter)
        except ValueError:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=(f"无效的状态值: {status_filter}，" f"可选值: draft, editing, rendering, completed, failed"),
            )

    # 项目鉴权：如果指定了 project_id，校验用户是否有权访问
    if project_id:
        _check_project_access(project_id, current_user.user.id, project_repository)

    skip = (page - 1) * page_size
    plans = svc.list_plans(
        template_id=template_id,
        project_id=project_id,
        status=status_enum,
        skip=skip,
        limit=page_size,
    )
    total = svc.count_plans(
        template_id=template_id,
        project_id=project_id,
        status=status_enum,
    )

    return EditPlanListResponse(
        items=[_to_response(p) for p in plans],
        total=total,
        page=page,
        page_size=page_size,
    )


@router.get("/{plan_id}", response_model=EditPlanResponse)
def get_plan(
    plan_id: str,
    db: Session = Depends(get_db_session),
    current_user: AuthenticatedUser = Depends(get_current_user),
    project_repository: Any = Depends(get_project_repository),
) -> EditPlanResponse:
    """获取单个剪辑计划详情"""
    svc = EditPlanService(db)
    try:
        plan = svc.get_plan_or_raise(plan_id)
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=str(exc),
        )
    # 项目鉴权
    if plan.project_id:
        _check_project_access(plan.project_id, current_user.user.id, project_repository)
    return _to_response(plan)


@router.post("", response_model=EditPlanResponse, status_code=status.HTTP_201_CREATED)
def create_plan(
    body: EditPlanCreateRequest,
    db: Session = Depends(get_db_session),
    current_user: AuthenticatedUser = Depends(get_current_user),
    project_repository: Any = Depends(get_project_repository),
) -> EditPlanResponse:
    """创建剪辑计划"""
    # 空串 project_id 统一为 ""
    project_id = (body.project_id or "").strip()
    # 项目鉴权
    if project_id:
        _check_project_access(project_id, current_user.user.id, project_repository)
    svc = EditPlanService(db)
    # 标准化 config，填充 cover/title/subtitle/bgm 默认值
    normalized_config = normalize_plan_config(body.config)
    try:
        created = svc.create_plan(
            template_id=body.template_id,
            name=body.name,
            config=normalized_config,
            total_duration=body.total_duration,
            project_id=project_id,
            created_by_user_id=current_user.user.id,
        )
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(exc),
        )
    logger.info(
        "创建剪辑计划: id=%s name=%s by user=%s",
        created.id,
        created.name,
        current_user.user.id,
    )
    return _to_response(created)


@router.put("/{plan_id}", response_model=EditPlanResponse)
def update_plan(
    plan_id: str,
    body: EditPlanUpdateRequest,
    db: Session = Depends(get_db_session),
    current_user: AuthenticatedUser = Depends(get_current_user),
    project_repository: Any = Depends(get_project_repository),
) -> EditPlanResponse:
    """更新剪辑计划（支持状态机流转）"""
    svc = EditPlanService(db)
    # 项目鉴权
    existing = svc.get_plan(plan_id)
    if existing is None:
        raise HTTPException(status_code=404, detail=f"剪辑计划不存在: {plan_id}")
    if existing.project_id:
        _check_project_access(existing.project_id, current_user.user.id, project_repository)

    # 基础字段更新
    try:
        if body.name is not None or body.config is not None or body.total_duration is not None:
            # 标准化 config（如果提供了）
            config_to_update = normalize_plan_config(body.config) if body.config is not None else None
            svc.update_plan(
                plan_id,
                name=body.name,
                config=config_to_update,
                total_duration=body.total_duration,
            )

        # 状态机流转
        if body.status is not None:
            try:
                target_status = EditPlanStatus(body.status)
            except ValueError:
                raise HTTPException(
                    status_code=status.HTTP_400_BAD_REQUEST,
                    detail=(f"无效的状态值: {body.status}，" f"可选值: draft, editing, rendering, completed, failed"),
                )
            svc.transition_status(plan_id, target_status)
    except ValueError as exc:
        err_msg = str(exc)
        if "不存在" in err_msg:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=err_msg,
            )
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=err_msg,
        )

    # 返回最新状态
    result = svc.get_plan_or_raise(plan_id)
    logger.info("更新剪辑计划: id=%s by user=%s", plan_id, current_user.user.id)
    return _to_response(result)


@router.delete("/{plan_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
def delete_plan(
    plan_id: str,
    db: Session = Depends(get_db_session),
    current_user: AuthenticatedUser = Depends(get_current_user),
    project_repository: Any = Depends(get_project_repository),
) -> None:
    """删除剪辑计划"""
    svc = EditPlanService(db)
    # 项目鉴权
    existing = svc.get_plan(plan_id)
    if existing and existing.project_id:
        _check_project_access(existing.project_id, current_user.user.id, project_repository)
    deleted = svc.delete_plan(plan_id)
    if not deleted:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"剪辑计划不存在: {plan_id}",
        )
    logger.info(
        "删除剪辑计划: id=%s by user=%s",
        plan_id,
        current_user.user.id,
    )


# ── 生成相关端点（任务 2.05） ─────────────────────────────────────────────────


@router.post("/{plan_id}/generate", response_model=EditPlanGenerateResponse)
def generate_plan(
    plan_id: str,
    db: Session = Depends(get_db_session),
    current_user: AuthenticatedUser = Depends(get_current_user),
    project_repository: Any = Depends(get_project_repository),
) -> EditPlanGenerateResponse:
    """触发剪辑计划渲染生成

    前置条件：计划状态必须为 editing，且至少有一个片段。
    流程：
    1. 验证计划状态为 editing
    2. 将 pending 片段标记为 ready
    3. 创建 GenerationTask
    4. 调度 Celery 任务 worker.render_edit_plan
    5. 将计划状态流转为 rendering
    """
    svc = EditPlanService(db)
    # 项目鉴权
    plan_check = svc.get_plan(plan_id)
    if plan_check is None:
        raise HTTPException(status_code=404, detail=f"剪辑计划不存在: {plan_id}")
    if plan_check.project_id:
        _check_project_access(plan_check.project_id, current_user.user.id, project_repository)

    # 检查是否可生成
    try:
        can_gen, reason = svc.can_generate(plan_id)
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=str(exc),
        )
    if not can_gen:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=reason,
        )

    # 将 pending 片段标记为 ready
    clip_count = svc.mark_clips_ready(plan_id)

    # 创建 GenerationTask
    gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
    gen_task_use_case = CreateGenerationTaskUseCase(gen_task_repo)
    plan = svc.get_plan_or_raise(plan_id)
    gen_task = gen_task_use_case.execute(
        CreateGenerationTaskCommand(
            project_id="",
            template_id=plan.template_id,
            created_by_user_id=current_user.user.id,
            source_edit_plan_id=plan_id,
        )
    )

    # 将 generation_task_id 存入 plan config
    svc.update_plan_config(plan_id, {"generation_task_id": gen_task.id})

    # 流转状态为 rendering
    svc.transition_status(plan_id, EditPlanStatus.RENDERING)

    # 调度 Celery 任务
    celery_app.send_task("worker.render_edit_plan", args=[plan_id])

    # 获取最新状态
    updated_plan = svc.get_plan_or_raise(plan_id)

    logger.info(
        "触发剪辑计划生成: plan_id=%s gen_task_id=%s clips=%d by user=%s",
        plan_id,
        gen_task.id,
        clip_count,
        current_user.user.id,
    )

    return EditPlanGenerateResponse(
        plan_id=plan_id,
        plan_status=updated_plan.status.value if hasattr(updated_plan.status, "value") else updated_plan.status,
        generation_task_id=gen_task.id,
        clip_count=clip_count,
    )


@router.get(
    "/{plan_id}/generation-status",
    response_model=EditPlanGenerationStatusResponse,
)
def get_generation_status(
    plan_id: str,
    db: Session = Depends(get_db_session),
    current_user: AuthenticatedUser = Depends(get_current_user),
    project_repository: Any = Depends(get_project_repository),
) -> EditPlanGenerationStatusResponse:
    """查询剪辑计划生成进度

    返回计划状态、关联的 GenerationTask ID、以及每个片段的状态。
    """
    svc = EditPlanService(db)
    try:
        gen_status = svc.get_generation_status(plan_id)
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=str(exc),
        )

    plan = gen_status["plan"]
    # 项目鉴权
    if plan.project_id:
        _check_project_access(plan.project_id, current_user.user.id, project_repository)
    clips = gen_status["clips"]

    clip_items = [
        ClipStatusItem(
            clip_id=c.id,
            clip_type=c.clip_type,
            order=c.order,
            status=c.status.value if hasattr(c.status, "value") else c.status,
            asset_id=c.asset_id or "",
            text_content=c.text_content or "",
            duration=c.duration,
        )
        for c in clips
    ]

    return EditPlanGenerationStatusResponse(
        plan_id=plan_id,
        plan_status=plan.status.value if hasattr(plan.status, "value") else plan.status,
        generation_task_id=gen_status["generation_task_id"],
        clips=clip_items,
    )


@router.get(
    "/{plan_id}/generations",
    response_model=EditPlanGenerationsResponse,
)
def list_plan_generations(
    plan_id: str,
    db: Session = Depends(get_db_session),
    current_user: AuthenticatedUser = Depends(get_current_user),
    project_repository: Any = Depends(get_project_repository),
) -> EditPlanGenerationsResponse:
    """查询剪辑计划关联的所有生成记录

    返回该剪辑计划触发的所有 GenerationTask，按创建时间倒序。
    """
    svc = EditPlanService(db)
    # 验证计划存在 + 项目鉴权
    plan = svc.get_plan_or_raise(plan_id)
    if plan.project_id:
        _check_project_access(plan.project_id, current_user.user.id, project_repository)

    gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
    tasks = gen_task_repo.list_by_source_edit_plan(plan_id)
    items = [
        GenerationTaskResponse(
            id=t.id,
            project_id=t.project_id,
            asset_library_id=t.asset_library_id,
            strategy_id=t.strategy_id,
            voice_library_id=t.voice_library_id,
            template_id=t.template_id,
            asset_ids=t.asset_ids,
            title_ids=t.title_ids,
            voice_ids=t.voice_ids,
            source_edit_plan_id=t.source_edit_plan_id or "",
            status=t.status.value if hasattr(t.status, "value") else t.status,
            progress=t.progress,
            result_count=t.result_count,
            error_message=t.error_message,
        )
        for t in tasks
    ]
    return EditPlanGenerationsResponse(items=items, total=len(items))


# ── AI 推荐 & 封面生成端点（任务 3.09） ────────────────────────────────────────


@router.post(
    "/{plan_id}/ai-recommend",
    response_model=AIRecommendResponse,
)
def ai_recommend_clips(
    plan_id: str,
    body: AIRecommendRequest,
    db: Session = Depends(get_db_session),
    current_user: AuthenticatedUser = Depends(get_current_user),
    project_repository: Any = Depends(get_project_repository),
) -> AIRecommendResponse:
    """AI 推荐片段方案

    调用 AI 服务分析素材，自动生成片段编排方案并写入剪辑计划。

    流程：
    1. 验证计划存在且状态为 draft/editing
    2. 调用 AI 推荐服务（当前为 stub，后续接入真实 AI）
    3. 清除计划现有片段，按推荐方案重新创建
    4. 更新计划 config（cover/title/subtitle/bgm）和 total_duration
    5. 返回推荐方案详情

    前端对接：
    - 请求体只需传 asset_ids（必填），editing_mode 和 target_duration 可选
    - 返回的 clips 可直接渲染到时间线
    - 返回的 config 包含推荐的封面/标题/字幕/BGM 配置
    """
    svc = EditPlanService(db)

    # 验证计划存在
    try:
        plan = svc.get_plan_or_raise(plan_id)
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=str(exc),
        )

    # 项目鉴权
    if plan.project_id:
        _check_project_access(plan.project_id, current_user.user.id, project_repository)

    # 验证状态：只允许 draft 或 editing
    plan_status = plan.status.value if hasattr(plan.status, "value") else plan.status
    if plan_status not in ("draft", "editing"):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"AI 推荐仅支持 draft/editing 状态的计划，当前状态: {plan_status}",
        )

    # 调用 AI 推荐服务（同步调用 stub，后续改为 Celery 异步）
    from apps.worker.worker_app.tasks.ai_tasks import run_ai_recommend

    result = run_ai_recommend(
        plan_id=plan_id,
        template_id=plan.template_id,
        asset_ids=body.asset_ids,
        editing_mode=body.editing_mode,
        target_duration=body.target_duration,
    )

    # ── 事务保护：清除 → 重建 → 更新 必须在同一逻辑事务中 ──
    # TODO: 当前各 repo 方法内部 commit()，无法真正回滚。
    #       后续重构 repo 为 flush() 模式后，此处改为统一 commit。
    try:
        # 清除现有片段
        svc.delete_all_clips(plan_id)

        # 按推荐方案创建新片段
        for clip_data in result["clips"]:
            svc.create_clip(
                plan_id=plan_id,
                clip_type=clip_data["clip_type"],
                order=clip_data["order"],
                text_content=clip_data.get("text_content", ""),
                duration=clip_data["duration"],
                transition_effect=clip_data.get("transition_effect", "cut"),
                asset_id=clip_data.get("asset_id", ""),
                start_time=clip_data.get("start_time", 0.0),
                config=clip_data.get("config", {}),
            )

        # 更新计划 config 和 total_duration
        normalized_config = normalize_plan_config(result.get("config", {}))
        svc.update_plan(
            plan_id,
            config=normalized_config,
            total_duration=result["total_duration"],
        )
    except Exception as exc:
        logger.exception("AI 推荐写入失败，plan_id=%s 数据可能不一致", plan_id)
        # 尝试回滚未提交的变更
        try:
            db.rollback()
        except Exception:
            pass
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"AI 推荐结果写入失败: {exc}",
        )

    logger.info(
        "AI 推荐片段方案: plan_id=%s clips=%d duration=%.1f by user=%s",
        plan_id,
        len(result["clips"]),
        result["total_duration"],
        current_user.user.id,
    )

    return AIRecommendResponse(
        plan_id=plan_id,
        clips=[
            AIRecommendClipItem(
                clip_type=c["clip_type"],
                order=c["order"],
                text_content=c.get("text_content", ""),
                duration=c["duration"],
                transition_effect=c.get("transition_effect", "cut"),
                asset_id=c.get("asset_id", ""),
                start_time=c.get("start_time", 0.0),
                config=c.get("config", {}),
            )
            for c in result["clips"]
        ],
        config=normalized_config,
        total_duration=result["total_duration"],
        confidence=result["confidence"],
    )


@router.post(
    "/{plan_id}/generate-cover",
    response_model=GenerateCoverResponse,
)
def generate_cover(
    plan_id: str,
    body: GenerateCoverRequest,
    db: Session = Depends(get_db_session),
    current_user: AuthenticatedUser = Depends(get_current_user),
    project_repository: Any = Depends(get_project_repository),
) -> GenerateCoverResponse:
    """AI 生成封面

    调用 AI 服务从视频中选帧或生成封面图，并更新计划 config.cover。

    流程：
    1. 验证计划存在
    2. 调用 AI 封面生成服务（当前为 stub，后续接入真实 AI）
    3. 更新 plan.config["cover"] 为生成的封面数据
    4. 返回封面数据

    前端对接：
    - cover_type=ai_frame: AI 智能选帧（默认）
    - cover_type=manual: 手动选帧，需传 frame_time
    - cover_type=upload: 用户上传，接口返回空 image_url，前端自行上传后更新
    - cover_type=ai_regenerate: AI 重新生成
    """
    svc = EditPlanService(db)

    # 验证计划存在
    try:
        plan = svc.get_plan_or_raise(plan_id)
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=str(exc),
        )

    # 项目鉴权
    if plan.project_id:
        _check_project_access(plan.project_id, current_user.user.id, project_repository)

    # 调用 AI 封面生成服务
    from apps.worker.worker_app.tasks.ai_tasks import run_generate_cover

    cover_data = run_generate_cover(
        plan_id=plan_id,
        asset_ids=body.asset_ids,
        cover_type=body.cover_type,
        frame_time=body.frame_time,
    )

    # 更新 plan.config["cover"]
    current_config = dict(plan.config)
    current_config["cover"] = cover_data
    normalized = normalize_plan_config(current_config)
    svc.update_plan_config(plan_id, {"cover": normalized["cover"]})

    logger.info(
        "AI 封面生成: plan_id=%s type=%s by user=%s",
        plan_id,
        body.cover_type,
        current_user.user.id,
    )

    return GenerateCoverResponse(
        plan_id=plan_id,
        cover=cover_data,
    )


# ── Timeline / Scene 端点（P2-6） ─────────────────────────────────────────────


class TimelineSceneResponse(BaseModel):
    """时间线场景"""

    scene: str = Field(..., description="场景描述")
    time: str = Field(..., description='时间范围，如 "0:00 - 0:05"')
    duration: float = Field(..., ge=0, description="时长（秒）")
    color: str = Field(..., description="展示颜色")
    clip_id: str = Field(default="", description="关联的片段 ID")
    clip_type: str = Field(default="", description="片段类型")


class TimelineResponse(BaseModel):
    """时间线响应"""

    plan_id: str
    total_duration: float
    scenes: List[TimelineSceneResponse]


# clip_type → 颜色映射
_CLIP_TYPE_COLORS = {
    "intro": "#6366f1",
    "title": "#6366f1",
    "product": "#818cf8",
    "showcase": "#10b981",
    "scene": "#10b981",
    "subtitle": "#f59e0b",
    "text": "#f59e0b",
    "cta": "#ef4444",
    "outro": "#ef4444",
    "voiceover": "#8b5cf6",
    "transition": "#64748b",
}

_DEFAULT_COLOR = "#6366f1"


def _format_time(seconds: float) -> str:
    """将秒数格式化为 M:SS"""
    m = int(seconds) // 60
    s = int(seconds) % 60
    return f"{m}:{s:02d}"


def _clip_type_to_scene_label(clip_type: str, text_content: str) -> str:
    """根据 clip_type 和 text_content 生成场景描述"""
    type_labels = {
        "intro": "开场",
        "title": "标题",
        "product": "产品展示",
        "showcase": "场景展示",
        "scene": "场景",
        "subtitle": "字幕",
        "text": "文字",
        "cta": "结尾 CTA",
        "outro": "结尾",
        "voiceover": "配音",
        "transition": "转场",
    }
    label = type_labels.get(clip_type, clip_type or "片段")
    if text_content:
        # 截取前 20 个字符作为副标题
        short = text_content[:20].strip()
        if short:
            return f"{label} - {short}"
    return label


@router.get(
    "/{plan_id}/timeline",
    response_model=TimelineResponse,
)
def get_plan_timeline(
    plan_id: str,
    db: Session = Depends(get_db_session),
    current_user: AuthenticatedUser = Depends(get_current_user),
    project_repository: Any = Depends(get_project_repository),
) -> TimelineResponse:
    """获取剪辑计划的时间线场景数据

    返回按计划片段排序的时间线场景列表，供前端 GeneratePage 渲染使用。
    """
    svc = EditPlanService(db)
    plan = svc.get_plan_or_raise(plan_id)
    # 项目鉴权
    if plan.project_id:
        _check_project_access(plan.project_id, current_user.user.id, project_repository)

    clips = svc.list_clips(plan_id=plan_id, skip=0, limit=200)
    # 按 order 排序
    clips.sort(key=lambda c: c.order)

    scenes: List[TimelineSceneResponse] = []
    current_time = 0.0

    for clip in clips:
        start = current_time
        end = start + clip.duration
        color = _CLIP_TYPE_COLORS.get(clip.clip_type, _DEFAULT_COLOR)
        scene_label = _clip_type_to_scene_label(clip.clip_type, clip.text_content)

        scenes.append(
            TimelineSceneResponse(
                scene=scene_label,
                time=f"{_format_time(start)} - {_format_time(end)}",
                duration=clip.duration,
                color=color,
                clip_id=clip.id,
                clip_type=clip.clip_type,
            )
        )
        current_time = end

    total_duration = sum(s.duration for s in scenes) or plan.total_duration

    return TimelineResponse(
        plan_id=plan_id,
        total_duration=total_duration,
        scenes=scenes,
    )
