from __future__ import annotations

from datetime import datetime
from types import SimpleNamespace

from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
from sqlalchemy import inspect as sa_inspect
from sqlalchemy.ext.asyncio import AsyncSession

from app.dependencies import get_current_user, get_db
from app.models.user import User
from app.enums.shot_replicate import (
    ModuleCodeEnum,
    ShotAnalysisStatusEnum,
    ShotReplicateStepCodeEnum,
    ShotSegmentAnalysisStatusEnum,
    ShotSegmentReplicateStatusEnum,
    ShotSegmentSourceModeEnum,
    ShotSplitStatusEnum,
    ShotTaskSetStatusEnum,
)
from app.schemas.shot_replicate import (
    ShotReplicateActionOut,
    ShotReplicateDeleteOut,
    ShotReplicateGenerateImagePromptRequest,
    ShotReplicateGenerateImageRequest,
    ShotReplicateGenerateVideoPromptRequest,
    ShotReplicateGenerateVideoRequest,
    ShotReplicateImagePromptUpdateRequest,
    ShotReplicateMaterialUpdateRequest,
    ShotReplicateSpecOut,
    ShotReplicateTaskDetailOut,
    ShotReplicateVideoPromptSchemaUpdateRequest,
    ShotSegmentDetailOut,
    ShotSegmentListOut,
    ShotSegmentReplicationCreateRequest,
    ShotSplitByAIOut,
    ShotSplitByAIRequest,
    ShotSplitCustomOut,
    ShotSplitCustomRequest,
    ShotTaskSetCreate,
    ShotTaskSetDetailOut,
    ShotTaskSetListOut,
)
from app.services.shot_replicate_flow_service import (
    _get_project_for_user,
    create_shot_replicate_project_from_segment,
    delete_shot_replicate_project,
    generate_image_from_prompt,
    generate_video_from_prompt,
    mark_shot_replicate_step_dispatch_failed,
    project_to_detail_out,
    submit_image_prompt_optimize,
    submit_video_prompt_optimize,
    update_shot_replicate_image_prompt,
    update_shot_replicate_material_input,
    update_shot_replicate_video_prompt_schema,
)
from app.services.shot_replicate_taskset_service import (
    create_custom_segment,
    create_segments_by_ai,
    create_task_set,
    get_segment_for_user,
    list_segments,
    list_task_sets,
    segment_detail,
    task_set_detail,
)
from app.services.module_generation_log_service import log_module_error, log_module_event_file
from app.services.module_async_recovery_service import (
    TASK_SHOT_IMAGE_PROMPT,
    TASK_SHOT_VIDEO_PROMPT,
    register_module_step_task,
    register_shot_segment_analysis_task,
    register_shot_split_task,
    register_shot_task_set_analysis_task,
)
from app.tasks.celery_app import celery_app

MODULE = ModuleCodeEnum.SHOT_REPLICATE.value



def _safe_user_id(user: object | None) -> str | None:
    """从 ORM 对象中安全取用户ID，避免 rollback/commit 后访问过期属性触发 MissingGreenlet。"""
    if user is None:
        return None
    try:
        value = getattr(user, "__dict__", {}).get("id")
        if value is not None:
            return str(value)
    except Exception:
        pass
    try:
        identity = sa_inspect(user).identity
        if identity:
            return str(identity[0])
    except Exception:
        pass
    return None


def _safe_user_is_admin(user: object | None) -> bool:
    """安全判断管理员身份；如果对象属性已过期，保守按普通用户处理。"""
    if user is None:
        return False
    try:
        data = getattr(user, "__dict__", {})
        if "is_admin" in data:
            return bool(data.get("is_admin"))
    except Exception:
        pass
    return False


def _user_context(user: object | None) -> SimpleNamespace:
    return SimpleNamespace(id=_safe_user_id(user), is_admin=_safe_user_is_admin(user))

router = APIRouter(prefix="/shot-replications", tags=["shot-replications"])




def _log_api_error(
    *,
    event_type: str,
    current_user: User | None = None,
    project_id: str | None = None,
    step_id: str | None = None,
    message: str | None = None,
    exc: BaseException | None = None,
    detail: dict | None = None,
) -> None:
    log_module_error(
        module=MODULE,
        event_type=event_type,
        project_id=project_id,
        step_id=step_id,
        user_id=_safe_user_id(current_user),
        message=message,
        detail=detail,
        exc=exc,
    )


def _log_api_exception_from_locals(exc: BaseException, local_values: dict, message: str) -> None:
    current_user = local_values.get("current_user")
    project_id = local_values.get("project_id_value") or local_values.get("project_id") or local_values.get("task_set_id") or local_values.get("task_set_id_value")
    step_id = local_values.get("step_id_value") or local_values.get("step_id") or local_values.get("segment_id") or local_values.get("segment_id_value")
    req = local_values.get("req")
    detail = {"api": local_values.get("__name__"), "request": req.model_dump() if hasattr(req, "model_dump") else str(req) if req is not None else None}
    _log_api_error(
        event_type="API_REQUEST_FAILED",
        current_user=current_user if isinstance(current_user, User) else None,
        project_id=str(project_id) if project_id else None,
        step_id=str(step_id) if step_id else None,
        message=message,
        detail=detail,
        exc=exc,
    )



def _ensure_celery_enabled(*, current_user: User | None = None, project_id: str | None = None, step_id: str | None = None) -> None:
    if celery_app is not None:
        return
    message = "Celery未启用：请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker"
    _log_api_error(
        event_type="CELERY_DISABLED",
        current_user=current_user,
        project_id=project_id,
        step_id=step_id,
        message=message,
        detail={"api": "shot_replicate"},
    )
    raise HTTPException(status_code=503, detail=message)

async def _reload_project_detail(db: AsyncSession, current_user: User, project_id: str) -> ShotReplicateTaskDetailOut:
    project = await _get_project_for_user(
        db,
        project_id=project_id,
        user=_user_context(current_user),
        for_update=False,
        populate_existing=True,
    )
    return await project_to_detail_out(db, project)


async def _mark_dispatch_failed_and_raise(
    db: AsyncSession,
    *,
    current_user: User,
    project_id: str,
    step_id: str | None,
    message: str,
) -> None:
    if step_id:
        try:
            await mark_shot_replicate_step_dispatch_failed(
                db,
                current_user=_user_context(current_user),
                project_id=project_id,
                step_id=step_id,
                error_message=message,
            )
            await db.commit()
        except Exception as exc:
            await db.rollback()
            _log_api_error(
                event_type="CELERY_DISPATCH_MARK_FAILED",
                current_user=current_user,
                project_id=project_id,
                step_id=step_id,
                message="Celery 投递失败后标记步骤失败也失败",
                detail={"dispatch_error": message},
                exc=exc,
            )
    log_module_error(
        module=MODULE,
        event_type="CELERY_DISPATCH_FAILED",
        project_id=project_id,
        step_id=step_id,
        user_id=_safe_user_id(current_user),
        message=message,
        detail={"reason": "celery_dispatch_failed"},
        error=message,
    )
    raise HTTPException(status_code=503, detail=message)


@router.get(
    "/spec",
    response_model=ShotReplicateSpecOut,
    summary="查询拆镜复刻模块状态枚举和步骤 JSON 结构说明",
    description="保留给调试和前端兜底读取。原业务接口已经在 Path、Query、Body 和响应模型字段上直接展示参数说明与枚举值。",
)
async def get_spec():
    return ShotReplicateSpecOut()


@router.post(
    "/task-sets",
    response_model=ShotTaskSetDetailOut,
    summary="创建拆镜总任务集并异步分析原视频",
    description=(
        "创建拆镜总任务集，保存原视频地址和时长，随后异步投递原视频 AI 分析任务。"
        "分析完成后会写入原视频内容、分类、受众和 AI 建议拆镜时间段。"
        "状态枚举直接见本接口响应字段：status、analysis_status、split_status。"
    ),
)
async def create_shot_task_set(
    req: ShotTaskSetCreate = Body(..., description="创建拆镜总任务集参数：原视频 URL、视频时长、标题和可选幂等键"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    _ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
    try:
        task_set = await create_task_set(db, current_user=current_user, req=req)
        task_set_id = task_set.id
        await db.commit()
    except HTTPException:
        await db.rollback()
        raise
    except Exception as exc:
        await db.rollback()
        _log_api_exception_from_locals(exc, locals(), f"创建拆镜总任务集失败: {exc}")
        raise HTTPException(status_code=500, detail=f"创建拆镜总任务集失败: {exc}")

    try:
        from app.tasks.shot_replicate_tasks import analyze_original_video

        await register_shot_task_set_analysis_task(task_set_id)
        analyze_original_video.apply_async(args=[task_set_id], queue="gen_chatapi_create", countdown=0)
    except Exception as exc:
        _log_api_error(
            event_type="CELERY_DISPATCH_FAILED",
            current_user=current_user,
            project_id=task_set_id,
            message=f"拆镜分析任务投递失败: {exc}",
            detail={"task_set_id": task_set_id, "task": "analyze_original_video"},
            exc=exc,
        )
        raise HTTPException(status_code=503, detail=f"拆镜分析任务投递失败: {exc}")

    return await task_set_detail(db, current_user=_user_context(current_user), task_set_id=task_set_id)


@router.get(
    "/task-sets",
    response_model=ShotTaskSetListOut,
    summary="查询拆镜总任务集列表",
)
async def list_shot_task_sets(
    status: ShotTaskSetStatusEnum | None = Query(None, description="总任务状态筛选：pending_analysis=等待分析，analyzing=分析中，analysis_completed=分析完成，analysis_failed=分析失败，splitting=拆镜中，split_completed=拆镜完成，partial_failed=部分失败，failed=失败，deleted=已软删"),
    analysis_status: ShotAnalysisStatusEnum | None = Query(None, description="原视频分析状态筛选：pending=待分析，processing=分析中，completed=分析完成，failed=分析失败"),
    split_status: ShotSplitStatusEnum | None = Query(None, description="拆镜状态筛选：none=尚未拆镜，pending=待拆镜，processing=拆镜中，completed=拆镜完成，failed=拆镜失败，retry_waiting=等待恢复重试"),
    keyword: str | None = Query(None, description="标题/原视频内容/分类/受众关键词，模糊搜索；普通用户只在自己的数据内搜索"),
    user_id: str | None = Query(None, description="管理员专用：按用户ID筛选；普通用户不可使用"),
    user_name: str | None = Query(None, description="管理员专用：按用户名模糊筛选；普通用户不可使用"),
    created_start: datetime | None = Query(None, description="管理员专用：创建时间开始，ISO datetime；普通用户不可使用"),
    created_end: datetime | None = Query(None, description="管理员专用：创建时间结束，ISO datetime；普通用户不可使用"),
    page: int = Query(1, ge=1, description="分页页码，从1开始"),
    page_size: int = Query(20, ge=1, le=100, description="每页数量，范围1-100"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    admin_only_params = {
        "user_id": user_id,
        "user_name": user_name,
        "created_start": created_start,
        "created_end": created_end,
    }
    if not _safe_user_is_admin(current_user) and any(value is not None and str(value).strip() != "" for value in admin_only_params.values()):
        raise HTTPException(status_code=403, detail="当前搜索条件仅管理员可用")

    return await list_task_sets(
        db,
        current_user=current_user,
        status=status.value if status else None,
        analysis_status=analysis_status.value if analysis_status else None,
        split_status=split_status.value if split_status else None,
        keyword=keyword,
        user_id=user_id,
        user_name=user_name,
        created_start=created_start,
        created_end=created_end,
        page=page,
        page_size=page_size,
    )


@router.get(
    "/task-sets/{task_set_id}",
    response_model=ShotTaskSetDetailOut,
    summary="获取拆镜总任务集详情",
    description="根据拆镜总任务集ID查询详情，包含原视频分析结果、AI 建议拆镜列表和当前总任务状态。",
)
async def get_shot_task_set(
    task_set_id: str = Path(..., description="拆镜总任务集ID，即 shot_replicate_task_sets.id"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    return await task_set_detail(db, current_user=_user_context(current_user), task_set_id=task_set_id)


@router.post(
    "/task-sets/{task_set_id}/split-by-ai",
    response_model=ShotSplitByAIOut,
    summary="按 AI 建议方案异步拆镜",
    description=(
        "基于原视频 AI 分析生成的建议时间段创建拆镜片段，并异步投递 ffmpeg 切割任务。"
        "selected_indices 不传时默认按全部 AI 建议拆镜；replace_existing=true 时会软删旧 AI 建议片段后重新创建。"
    ),
)
async def split_by_ai(
    task_set_id: str = Path(..., description="拆镜总任务集ID，即 shot_replicate_task_sets.id"),
    req: ShotSplitByAIRequest = Body(default_factory=ShotSplitByAIRequest, description="AI 建议拆镜参数：可选择建议序号，也可选择是否覆盖旧 AI 片段"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    _ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
    try:
        out = await create_segments_by_ai(db, current_user=current_user, task_set_id=task_set_id, req=req)
        segment_ids = [item.id for item in out.segments]
        await db.commit()
    except HTTPException:
        await db.rollback()
        raise
    except Exception as exc:
        await db.rollback()
        _log_api_exception_from_locals(exc, locals(), f"按 AI 建议拆镜失败: {exc}")
        raise HTTPException(status_code=500, detail=f"按 AI 建议拆镜失败: {exc}")

    from app.tasks.shot_replicate_tasks import split_one_segment

    for segment_id in segment_ids:
        await register_shot_split_task(segment_id, task_set_id=task_set_id)
        split_one_segment.apply_async(args=[segment_id], queue="gen_result_download", countdown=0)
    return out


@router.post(
    "/task-sets/{task_set_id}/split-custom",
    response_model=ShotSplitCustomOut,
    summary="按用户自定义开始/结束秒异步拆单条片段",
    description="按用户传入的 start_second/end_second 创建 custom 来源片段，并异步投递 ffmpeg 切割任务；切割完成后可进入复刻项目。",
)
async def split_custom(
    task_set_id: str = Path(..., description="拆镜总任务集ID，即 shot_replicate_task_sets.id"),
    req: ShotSplitCustomRequest = Body(..., description="自定义拆镜时间段参数，end_second 必须大于 start_second"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    _ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
    try:
        out = await create_custom_segment(db, current_user=current_user, task_set_id=task_set_id, req=req)
        segment_id = out.segment.id
        await db.commit()
    except HTTPException:
        await db.rollback()
        raise
    except Exception as exc:
        await db.rollback()
        _log_api_exception_from_locals(exc, locals(), f"自定义拆镜失败: {exc}")
        raise HTTPException(status_code=500, detail=f"自定义拆镜失败: {exc}")

    from app.tasks.shot_replicate_tasks import split_one_segment

    await register_shot_split_task(segment_id, task_set_id=task_set_id)
    split_one_segment.apply_async(args=[segment_id], queue="gen_result_download", countdown=0)
    return out


@router.get(
    "/task-sets/{task_set_id}/segments",
    response_model=ShotSegmentListOut,
    summary="查询拆镜片段列表",
    description="分页查询指定拆镜总任务集下的片段列表，可按来源、切割状态、片段分析状态和复刻状态筛选。",
)
async def list_task_set_segments(
    task_set_id: str = Path(..., description="拆镜总任务集ID，即 shot_replicate_task_sets.id"),
    source_mode: ShotSegmentSourceModeEnum | None = Query(None, description="片段来源：ai_suggestion=AI 建议拆镜，custom=用户自定义拆镜"),
    split_status: ShotSplitStatusEnum | None = Query(None, description="切割状态：none=尚未拆镜，pending=待拆镜，processing=拆镜中，completed=拆镜完成，failed=拆镜失败，retry_waiting=等待恢复重试"),
    analysis_status: ShotSegmentAnalysisStatusEnum | None = Query(None, description="片段分析状态：not_required=无需单独分析，pending=等待分析，processing=分析中，completed=分析完成，failed=分析失败"),
    replicate_status: ShotSegmentReplicateStatusEnum | None = Query(None, description="片段复刻状态：not_started=未复刻，project_created=已创建复刻项目，processing=复刻处理中，completed=复刻完成，failed=复刻失败"),
    page: int = Query(1, ge=1, description="分页页码，从1开始"),
    page_size: int = Query(20, ge=1, le=100, description="每页数量，范围1-100"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    return await list_segments(
        db,
        current_user=current_user,
        task_set_id=task_set_id,
        source_mode=source_mode.value if source_mode else None,
        split_status=split_status.value if split_status else None,
        analysis_status=analysis_status.value if analysis_status else None,
        replicate_status=replicate_status.value if replicate_status else None,
        page=page,
        page_size=page_size,
    )


@router.get(
    "/segments/{segment_id}",
    response_model=ShotSegmentDetailOut,
    summary="获取拆镜片段详情",
    description="根据拆镜片段ID查询单个片段详情，包含切割结果、片段分析结果、复刻项目ID和状态。",
)
async def get_segment(
    segment_id: str = Path(..., description="拆镜片段ID，即 shot_replicate_segments.id"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    return await segment_detail(db, current_user=current_user, segment_id=segment_id)


@router.post(
    "/segments/{segment_id}/replication-projects",
    response_model=ShotReplicateActionOut,
    summary="将拆镜片段创建为拆镜复刻项目",
    description=(
        "以拆镜片段作为锁定素材视频创建 ModuleGenerationProject 复刻项目。"
        "创建后只同步生成第1步 material_input，后续第2-5步需要调用 /projects/{project_id}/steps/{step_id}/... 系列接口手动推进。"
        "素材视频来自片段 segment_video_url，不允许前端传入或后续修改。"
    ),
)
async def create_replication_project_from_segment(
    segment_id: str = Path(..., description="拆镜片段ID，即 shot_replicate_segments.id"),
    req: ShotSegmentReplicationCreateRequest = Body(..., description="从拆镜片段创建复刻项目参数：生成项目名称、核心内容点、新产品图片和可选幂等键"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    try:
        segment = await get_segment_for_user(db, segment_id=segment_id, user=current_user, for_update=True)
        project = await create_shot_replicate_project_from_segment(db, current_user=current_user, segment=segment, req=req)
        project_id = project.id
        await db.commit()
    except HTTPException:
        await db.rollback()
        raise
    except Exception as exc:
        await db.rollback()
        _log_api_exception_from_locals(exc, locals(), f"创建拆镜复刻项目失败: {exc}")
        raise HTTPException(status_code=500, detail=f"创建拆镜复刻项目失败: {exc}")

    return ShotReplicateActionOut(
        message="已从拆镜片段创建复刻项目，素材视频已锁定",
        project_id=project_id,
        step_id=None,
        detail=await _reload_project_detail(db, current_user, project_id),
    )


@router.get(
    "/projects/{project_id}",
    response_model=ShotReplicateTaskDetailOut,
    summary="获取拆镜复刻项目详情",
    description="获取拆镜复刻 ModuleGenerationProject 项目详情，聚合返回素材、图片提词、图片生成、视频提词、视频生成和当前有效步骤列表。",
)
async def get_project(
    project_id: str = Path(..., description="拆镜复刻项目ID，即 module_generation_projects.id"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    return await _reload_project_detail(db, current_user, project_id)


@router.put(
    "/projects/{project_id}/material",
    response_model=ShotReplicateActionOut,
    summary="修改拆镜复刻素材信息，素材视频不允许修改",
    description=(
        "修改拆镜复刻第1步素材输入，并重建第1步 material_input 新版本。"
        "素材视频 material_video_url 锁定为拆镜片段视频，不允许修改；可修改新产品图片、参考素材项目名、生成项目名和核心内容点。"
        "修改后会软删除第2、3、4、5步当前有效任务，并清空旧图片/视频结果。"
    ),
)
async def update_material(
    project_id: str = Path(..., description="拆镜复刻项目ID，即 module_generation_projects.id"),
    req: ShotReplicateMaterialUpdateRequest = Body(..., description="第1步素材输入修改参数；不接收 material_video_url，至少传一个允许修改字段"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    try:
        project_id_value, step_id_value = await update_shot_replicate_material_input(db, current_user=current_user, project_id=project_id, req=req)
        await db.commit()
    except HTTPException:
        await db.rollback()
        raise
    except Exception as exc:
        await db.rollback()
        _log_api_exception_from_locals(exc, locals(), f"修改素材输入失败: {exc}")
        raise HTTPException(status_code=500, detail=f"修改素材输入失败: {exc}")
    return ShotReplicateActionOut(message="素材输入已修改，素材视频保持锁定", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))


@router.put(
    "/projects/{project_id}/steps/{step_id}/image-prompt",
    response_model=ShotReplicateActionOut,
    summary="直接修改第2步图片 AI 优化提词",
    description="直接修改第2步 image_prompt_optimize 的图片提示词，不调用 AI、不扣积分；保存后软删除第3、4、5步当前有效任务。",
)
async def update_image_prompt(
    project_id: str = Path(..., description="拆镜复刻项目ID，即 module_generation_projects.id"),
    step_id: str = Path(..., description="第2步图片 AI 提词子任务ID，即 module_generation_steps.id，step_code=image_prompt_optimize"),
    req: ShotReplicateImagePromptUpdateRequest = Body(..., description="图片 AI 优化提词修改参数"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    try:
        project, step = await update_shot_replicate_image_prompt(db, current_user=current_user, project_id=project_id, step_id=step_id, req=req)
        project_id_value, step_id_value = project.id, step.id
        await db.commit()
    except HTTPException:
        await db.rollback()
        raise
    except Exception as exc:
        await db.rollback()
        _log_api_exception_from_locals(exc, locals(), f"修改图片 AI 提词失败: {exc}")
        raise HTTPException(status_code=500, detail=f"修改图片 AI 提词失败: {exc}")
    return ShotReplicateActionOut(message="图片 AI 提词已修改，后续步骤已软删除", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))


@router.put(
    "/projects/{project_id}/steps/{step_id}/video-prompt-schema",
    response_model=ShotReplicateActionOut,
    summary="修改第4步视频 AI 提词 JSON schema",
    description=(
        "修改第4步 video_prompt_optimize 的视频提词 JSON schema，不调用 AI、不扣积分。"
        "服务端会锁定视频时长、比例、分辨率、帧率、动态时间规划等关键结构；保存后软删除第5步视频生成任务。"
    ),
)
async def update_video_prompt_schema(
    project_id: str = Path(..., description="拆镜复刻项目ID，即 module_generation_projects.id"),
    step_id: str = Path(..., description="第4步视频 AI 提词子任务ID，即 module_generation_steps.id，step_code=video_prompt_optimize"),
    req: ShotReplicateVideoPromptSchemaUpdateRequest = Body(..., description="视频 AI 提词 JSON schema 修改参数"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    try:
        project, step = await update_shot_replicate_video_prompt_schema(db, current_user=current_user, project_id=project_id, step_id=step_id, req=req)
        project_id_value, step_id_value = project.id, step.id
        await db.commit()
    except HTTPException:
        await db.rollback()
        raise
    except Exception as exc:
        await db.rollback()
        _log_api_exception_from_locals(exc, locals(), f"修改视频 AI 提词失败: {exc}")
        raise HTTPException(status_code=500, detail=f"修改视频 AI 提词失败: {exc}")
    return ShotReplicateActionOut(message="视频 AI 提词 schema 已修改，第5步视频生成已软删除", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))


@router.post(
    "/projects/{project_id}/steps/{step_id}/generate-image-prompt",
    response_model=ShotReplicateActionOut,
    summary="基于第1步素材输入生成图片 AI 提词",
    description=(
        "基于第1步 material_input 子任务手动生成第2步 image_prompt_optimize。"
        "如果已存在旧的第2、3、4、5步，会先软删除旧步骤，再创建新的第2步并投递 Celery 文本提词任务。"
    ),
)
async def generate_image_prompt(
    project_id: str = Path(..., description="拆镜复刻项目ID，即 module_generation_projects.id"),
    step_id: str = Path(..., description="第1步素材输入子任务ID，即 module_generation_steps.id，step_code=material_input"),
    req: ShotReplicateGenerateImagePromptRequest = Body(default_factory=ShotReplicateGenerateImagePromptRequest, description="图片提词生成参数，当前无需传参，额外字段会忽略"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    _ensure_celery_enabled(current_user=current_user, project_id=project_id, step_id=step_id)
    try:
        project, step = await submit_image_prompt_optimize(db, current_user=current_user, project_id=project_id, material_step_id=step_id, req=req)
        project_id_value, step_id_value = project.id, step.id
        await db.commit()
    except HTTPException:
        await db.rollback()
        raise
    except Exception as exc:
        await db.rollback()
        _log_api_exception_from_locals(exc, locals(), f"提交图片 AI 提词失败: {exc}")
        raise HTTPException(status_code=500, detail=f"提交图片 AI 提词失败: {exc}")

    try:
        from app.tasks.shot_replicate_flow_tasks import start_image_prompt_optimize

        await register_module_step_task(
            module=MODULE,
            project_id=project_id_value,
            step_id=step_id_value,
            step_code=ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
            task_name=TASK_SHOT_IMAGE_PROMPT,
        )
        start_image_prompt_optimize.apply_async(args=[project_id_value, step_id_value], queue="gen_chatapi_create", countdown=0)
    except Exception as exc:
        await _mark_dispatch_failed_and_raise(db, current_user=current_user, project_id=project_id_value, step_id=step_id_value, message=f"图片 AI 提词任务投递失败: {exc}")

    return ShotReplicateActionOut(message="图片 AI 提词任务已提交", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))


@router.post(
    "/projects/{project_id}/steps/{step_id}/generate-image",
    response_model=ShotReplicateActionOut,
    summary="基于第2步图片 AI 提词生成图片",
    description=(
        "基于第2步 image_prompt_optimize 子任务生成第3步 image_generate。"
        "请求体传入图片引擎和图片参数；ChatGenerationTask 幂等键由后端自动生成。"
        "如果已存在旧的第3、4、5步，会先软删除旧步骤，再创建新的第3步。"
    ),
)
async def generate_image(
    project_id: str = Path(..., description="拆镜复刻项目ID，即 module_generation_projects.id"),
    step_id: str = Path(..., description="第2步图片 AI 提词子任务ID，即 module_generation_steps.id，step_code=image_prompt_optimize"),
    req: ShotReplicateGenerateImageRequest = Body(..., description="图片生成引擎和参数；可选值来自图片引擎配置接口"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    _ensure_celery_enabled(current_user=current_user, project_id=project_id, step_id=step_id)
    try:
        project, step, chat_task = await generate_image_from_prompt(db, current_user=current_user, project_id=project_id, prompt_step_id=step_id, req=req)
        project_id_value, step_id_value, chat_task_id_value = project.id, step.id, chat_task.id
        await db.commit()
    except HTTPException:
        await db.rollback()
        raise
    except Exception as exc:
        await db.rollback()
        _log_api_exception_from_locals(exc, locals(), f"提交图片生成失败: {exc}")
        raise HTTPException(status_code=500, detail=f"提交图片生成失败: {exc}")

    try:
        from app.tasks.generation_create_tasks import chatapi_create_generation_task

        chatapi_create_generation_task.apply_async(args=[chat_task_id_value], queue="gen_chatapi_create", countdown=0)
    except Exception as exc:
        await _mark_dispatch_failed_and_raise(db, current_user=current_user, project_id=project_id_value, step_id=step_id_value, message=f"图片生成任务投递失败: {exc}")

    return ShotReplicateActionOut(message="图片生成任务已提交", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))


@router.post(
    "/projects/{project_id}/steps/{step_id}/generate-video-prompt",
    response_model=ShotReplicateActionOut,
    summary="基于第3步图片结果生成视频 AI 提词 JSON schema",
    description=(
        "基于第3步 image_generate 子任务生成第4步 video_prompt_optimize。"
        "视频时长、比例、分辨率在本步骤确定并写入第4步 output_json；第5步视频生成只选择视频引擎。"
        "如果已存在旧的第4、5步，会先软删除旧步骤，再创建新的第4步并投递 Celery 文本提词任务。"
    ),
)
async def generate_video_prompt(
    project_id: str = Path(..., description="拆镜复刻项目ID，即 module_generation_projects.id"),
    step_id: str = Path(..., description="第3步图片生成子任务ID，即 module_generation_steps.id，step_code=image_generate"),
    req: ShotReplicateGenerateVideoPromptRequest = Body(..., description="视频提词生成参数：视频引擎、时长、比例、分辨率和目标平台；可选值来自视频引擎配置接口"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    _ensure_celery_enabled(current_user=current_user, project_id=project_id, step_id=step_id)
    try:
        project, step = await submit_video_prompt_optimize(db, current_user=current_user, project_id=project_id, image_step_id=step_id, req=req)
        project_id_value, step_id_value = project.id, step.id
        await db.commit()
    except HTTPException:
        await db.rollback()
        raise
    except Exception as exc:
        await db.rollback()
        _log_api_exception_from_locals(exc, locals(), f"提交视频 AI 提词失败: {exc}")
        raise HTTPException(status_code=500, detail=f"提交视频 AI 提词失败: {exc}")

    try:
        from app.tasks.shot_replicate_flow_tasks import start_video_prompt_optimize

        await register_module_step_task(
            module=MODULE,
            project_id=project_id_value,
            step_id=step_id_value,
            step_code=ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
            task_name=TASK_SHOT_VIDEO_PROMPT,
        )
        start_video_prompt_optimize.apply_async(args=[project_id_value, step_id_value], queue="gen_chatapi_create", countdown=0)
    except Exception as exc:
        await _mark_dispatch_failed_and_raise(db, current_user=current_user, project_id=project_id_value, step_id=step_id_value, message=f"视频 AI 提词任务投递失败: {exc}")

    return ShotReplicateActionOut(message="视频 AI 提词任务已提交", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))


@router.post(
    "/projects/{project_id}/steps/{step_id}/generate-video",
    response_model=ShotReplicateActionOut,
    summary="基于第4步视频 AI 提词生成最终视频",
    description=(
        "基于第4步 video_prompt_optimize 子任务生成第5步 video_generate。"
        "请求体只需要选择视频生成引擎 engine_id；duration、aspect_ratio、resolution 从第4步视频提词结果继承。"
        "如果已存在旧的第5步，会先软删除旧步骤，再创建新的第5步。"
    ),
)
async def generate_video(
    project_id: str = Path(..., description="拆镜复刻项目ID，即 module_generation_projects.id"),
    step_id: str = Path(..., description="第4步视频 AI 提词子任务ID，即 module_generation_steps.id，step_code=video_prompt_optimize"),
    req: ShotReplicateGenerateVideoRequest = Body(..., description="视频生成参数：只传 engine_id，其它视频参数继承第4步视频提词结果"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    _ensure_celery_enabled(current_user=current_user, project_id=project_id, step_id=step_id)
    try:
        project, step, chat_task = await generate_video_from_prompt(db, current_user=current_user, project_id=project_id, prompt_step_id=step_id, req=req)
        project_id_value, step_id_value, chat_task_id_value = project.id, step.id, chat_task.id
        await db.commit()
    except HTTPException:
        await db.rollback()
        raise
    except Exception as exc:
        await db.rollback()
        _log_api_exception_from_locals(exc, locals(), f"提交视频生成失败: {exc}")
        raise HTTPException(status_code=500, detail=f"提交视频生成失败: {exc}")

    try:
        from app.tasks.generation_create_tasks import chatapi_create_generation_task

        chatapi_create_generation_task.apply_async(args=[chat_task_id_value], queue="gen_chatapi_create", countdown=0)
    except Exception as exc:
        await _mark_dispatch_failed_and_raise(db, current_user=current_user, project_id=project_id_value, step_id=step_id_value, message=f"视频生成任务投递失败: {exc}")

    return ShotReplicateActionOut(message="视频生成任务已提交", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))


@router.delete(
    "/projects/{project_id}",
    response_model=ShotReplicateDeleteOut,
    summary="软删除拆镜复刻项目",
    description="软删除拆镜复刻 ModuleGenerationProject，并联动软删除当前有效步骤和关联的 ChatGenerationTask。",
)
async def delete_project(
    project_id: str = Path(..., description="拆镜复刻项目ID，即 module_generation_projects.id"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    try:
        out = await delete_shot_replicate_project(db, current_user=current_user, project_id=project_id)
        await db.commit()
        return out
    except HTTPException:
        await db.rollback()
        raise
    except Exception as exc:
        await db.rollback()
        _log_api_exception_from_locals(exc, locals(), f"删除拆镜复刻项目失败: {exc}")
        raise HTTPException(status_code=500, detail=f"删除拆镜复刻项目失败: {exc}")
