package com.yaoai.agent.service.impl;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import cn.dev33.satoken.stp.StpUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yaoai.agent.dto.AgentRunCommand;
import com.yaoai.agent.service.AgentRunService;
import com.yaoai.agent.service.ScriptUnderstandingService;
import com.yaoai.agent.sse.AgentSseManager;
import com.yaoai.ai.core.model.ChatMessage;
import com.yaoai.ai.core.model.ChatRequest;
import com.yaoai.ai.core.service.LlmService;
import com.yaoai.common.context.UserContext;
import com.yaoai.common.exception.BizException;
import com.yaoai.common.exception.ErrorCode;
import com.yaoai.domain.entity.AgentRunPlan;
import com.yaoai.domain.entity.AiTask;
import com.yaoai.domain.entity.AssemblyTask;
import com.yaoai.domain.entity.Episode;
import com.yaoai.domain.entity.GraphRun;
import com.yaoai.domain.entity.GraphStep;
import com.yaoai.domain.entity.Outline;
import com.yaoai.domain.entity.Scene;
import com.yaoai.domain.entity.ScriptUnderstanding;
import com.yaoai.domain.entity.Storyboard;
import com.yaoai.domain.mapper.AiTaskMapper;
import com.yaoai.domain.mapper.AgentRunPlanMapper;
import com.yaoai.domain.mapper.AssemblyTaskMapper;
import com.yaoai.domain.mapper.CharacterMapper;
import com.yaoai.domain.mapper.EpisodeMapper;
import com.yaoai.domain.mapper.GraphRunMapper;
import com.yaoai.domain.mapper.GraphStepMapper;
import com.yaoai.domain.mapper.OutlineMapper;
import com.yaoai.domain.mapper.SceneMapper;
import com.yaoai.domain.mapper.StoryboardMapper;
import com.yaoai.pipeline.service.AssemblyPipelineService;
import com.yaoai.pipeline.service.AssetGenPipelineService;
import com.yaoai.pipeline.service.OutlinePipelineService;
import com.yaoai.pipeline.service.StoryboardPipelineService;
import com.yaoai.pipeline.service.VideoTaskPipelineService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import jakarta.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Supplier;

@Slf4j
@Service
@RequiredArgsConstructor
public class AgentRunServiceImpl implements AgentRunService {

    private final GraphRunMapper graphRunMapper;
    private final GraphStepMapper graphStepMapper;
    private final AiTaskMapper aiTaskMapper;
    private final AssemblyTaskMapper assemblyTaskMapper;
    private final AgentRunPlanMapper agentRunPlanMapper;
    private final OutlineMapper outlineMapper;
    private final EpisodeMapper episodeMapper;
    private final CharacterMapper characterMapper;
    private final SceneMapper sceneMapper;
    private final StoryboardMapper storyboardMapper;
    private final AgentSseManager sseManager;
    private final LlmService llmService;
    private final ObjectMapper objectMapper;
    private final ScriptUnderstandingService scriptUnderstandingService;
    private final OutlinePipelineService outlinePipeline;
    private final AssetGenPipelineService assetGenPipeline;
    private final StoryboardPipelineService storyboardPipeline;
    private final VideoTaskPipelineService videoTaskPipeline;
    private final AssemblyPipelineService assemblyPipeline;

    @Resource(name = "agentExecutor")
    private Executor agentExecutor;

    // ─── Step 定义 ─────────────────────────────────────────────────────────────

    record StepDef(String key, String name, String agentType) {}

    private static final StepDef SCRIPT_UNDERSTANDING_STEP =
            new StepDef("script_understanding", "剧情理解与事实抽取", "EXECUTOR");
    private static final List<StepDef> BASE_STEP_DEFS = List.of(
            new StepDef("decision",      "决策层分析目标",        "DECISION"),
            new StepDef("outline_gen",   "大纲生成 SubAgent",     "EXECUTOR"),
            new StepDef("episode_gen",   "分集脚本 SubAgent",     "EXECUTOR"),
            new StepDef("supervision_1", "监督层审核（剧本）",    "SUPERVISION"),
            new StepDef("asset_gen",     "角色/场景提取 SubAgent","EXECUTOR"),
            new StepDef("storyboard_gen","分镜生成 SubAgent",     "EXECUTOR"),
            new StepDef("supervision_2", "监督层终审",            "SUPERVISION")
    );
    private static final List<StepDef> QUICK_VIDEO_STEP_DEFS = List.of(
            new StepDef("image_generation", "分镜图准备", "EXECUTOR"),
            new StepDef("video_generation", "视频生成任务", "EXECUTOR"),
            new StepDef("video_composition", "合成成片", "EXECUTOR")
    );

    private static final String QUICK_VIDEO_INTENT = "quick_video";
    private static final Duration QUICK_VIDEO_WAIT_TIMEOUT = Duration.ofMinutes(30);
    private static final Duration QUICK_VIDEO_ASSEMBLY_TIMEOUT = Duration.ofMinutes(10);
    private static final Duration QUICK_VIDEO_POLL_INTERVAL = Duration.ofSeconds(10);

    // ─── Public API ────────────────────────────────────────────────────────────

    @Override
    public GraphRun startRun(Long projectId, Long tenantId, String goal) {
        AgentRunCommand command = new AgentRunCommand();
        command.setGoal(goal);
        return startRun(projectId, tenantId, command);
    }

    @Override
    public GraphRun startRun(Long projectId, Long tenantId, AgentRunCommand command) {
        Long userId = StpUtil.getLoginIdAsLong();
        AgentRunCommand normalized = normalize(command);
        boolean hasScript = normalized.getContextOptions().useScriptAsset()
                && scriptUnderstandingService.latestScriptAsset(projectId, tenantId).isPresent();
        List<StepDef> plannedSteps = planSteps(hasScript, normalized);

        GraphRun run = new GraphRun();
        run.setProjectId(projectId);
        run.setTenantId(tenantId);
        run.setGoal(normalized.getGoal());
        run.setStatus("RUNNING");
        graphRunMapper.insert(run);

        persistRunPlan(run.getId(), runMode(hasScript, normalized), plannedSteps, normalized);

        for (int i = 0; i < plannedSteps.size(); i++) {
            StepDef def = plannedSteps.get(i);
            GraphStep step = new GraphStep();
            step.setRunId(run.getId());
            step.setStepKey(def.key());
            step.setStepName(def.name());
            step.setAgentType(def.agentType());
            step.setStatus("PENDING");
            step.setSeq(i);
            graphStepMapper.insert(step);
        }

        Long runId = run.getId();
        agentExecutor.execute(() -> executeRun(runId, projectId, tenantId, userId, normalized, hasScript));
        return run;
    }

    @Override
    public GraphRun getRun(Long runId, Long tenantId) {
        GraphRun run = graphRunMapper.selectById(runId);
        if (run == null || !run.getTenantId().equals(tenantId)) {
            throw new BizException(ErrorCode.NOT_FOUND, "Run 不存在");
        }
        return run;
    }

    @Override
    public GraphRun getLatestRun(Long projectId, Long tenantId) {
        return graphRunMapper.findLatestByProject(projectId, tenantId).orElse(null);
    }

    @Override
    public List<GraphStep> getSteps(Long runId, Long tenantId) {
        return graphStepMapper.findByRunId(runId);
    }

    @Override
    public void pauseRun(Long runId, Long tenantId) {
        updateRunStatus(runId, "PAUSED");
        sseManager.emit(runId, "run.paused", Map.of());
    }

    @Override
    public void resumeRun(Long runId, Long tenantId) {
        updateRunStatus(runId, "RUNNING");
        sseManager.emit(runId, "run.resumed", Map.of());
    }

    // ─── Async Executor ────────────────────────────────────────────────────────

    private void executeRun(Long runId, Long projectId, Long tenantId, Long userId,
                            AgentRunCommand command, boolean hasScript) {
        log.info("Agent run started: runId={}, projectId={}", runId, projectId);
        UserContext.set(userId);
        try {
            String goal = command.getGoal();
            AgentRunCommand.ContextOptions options = command.getContextOptions();
            ScriptUnderstanding understanding = null;

            if (hasStep(runId, "script_understanding")) {
                understanding = (ScriptUnderstanding) executeStep(runId, "script_understanding", () -> {
                    sseManager.emitLog(runId, "agent", "[剧情理解] 读取剧本资产，抽取剧情事实与证据...");
                    ScriptUnderstanding result = scriptUnderstandingService.understandLatestScript(projectId, tenantId, userId, goal);
                    sseManager.emitLog(runId, "info", "剧情理解完成，状态：" + result.getQualityStatus());
                    return result;
                });
            } else if (hasStep(runId, "decision")) {
                executeStep(runId, "decision", () -> {
                    sseManager.emitLog(runId, "agent", "[决策层] 无剧本资产，按目标创作链路规划执行...");
                    llmService.chat(ChatRequest.builder()
                            .messages(List.of(
                                    ChatMessage.system(DECISION_SYSTEM),
                                    ChatMessage.user("用户目标：" + goal)
                            )).build());
                    sseManager.emitLog(runId, "agent", "[决策层] 执行计划已制定");
                    return "目标分析完成。执行计划：大纲生成 → 分集脚本 → 质量审核 → 资产提取 → 分镜生成 → 终审";
                });
            }

            checkPaused(runId);

            if (command.shouldForceRegenerate()) {
                purgeAllGeneratedContent(runId, projectId, tenantId);
            }

            Outline existingOutline = outlinePipeline.getOutline(projectId, tenantId);
            Outline outline;
            if (existingOutline != null && options.useExistingOutline() && !command.shouldForceRegenerate()) {
                outline = existingOutline;
                skipStep(runId, "outline_gen", "已存在大纲，按上下文策略跳过重生成：《" + outline.getTitle() + "》");
            } else {
                if (!command.shouldForceRegenerate() && existingOutline != null && !options.useExistingOutline()) {
                    purgeAllGeneratedContent(runId, projectId, tenantId);
                }
                ScriptUnderstanding finalUnderstanding = understanding;
                outline = (Outline) executeStep(runId, "outline_gen", () -> {
                sseManager.emitLog(runId, "agent", "[执行层] 生成大纲...");
                String outlineContext = scriptUnderstandingService.toGenerationContext(finalUnderstanding, goal);
                Outline o = outlinePipeline.generateOutline(projectId, tenantId, outlineContext);
                sseManager.emitLog(runId, "info", "大纲已保存：" + o.getTitle());
                return o;
                });
            }

            checkPaused(runId);

            List<Episode> existingEpisodes = episodeMapper.findByProject(projectId, tenantId);
            List<Episode> episodes;
            if (!existingEpisodes.isEmpty() && options.useExistingEpisodes() && !command.shouldForceRegenerate()) {
                episodes = existingEpisodes;
                skipStep(runId, "episode_gen", "已存在 " + episodes.size() + " 集分集，按上下文策略跳过重生成");
            } else {
                if (!command.shouldForceRegenerate() && !existingEpisodes.isEmpty() && !options.useExistingEpisodes()) {
                    purgeEpisodesAndStoryboards(runId, projectId, tenantId);
                }
                Outline finalOutline = outline;
                episodes = (List<Episode>) executeStep(runId, "episode_gen", () -> {
                sseManager.emitLog(runId, "agent", "[执行层] 生成分集脚本...");
                List<Episode> eps = outlinePipeline.generateEpisodes(
                        finalOutline.getId(), projectId, tenantId);
                sseManager.emitLog(runId, "info", "已生成 " + eps.size() + " 集");
                return eps;
                });
            }

            checkPaused(runId);

            // Step 4: Supervision 1
            executeSupervisionStep(runId, "supervision_1", projectId, tenantId,
                    "剧本质量审核：大纲「" + outline.getTitle() + "」，共 " + episodes.size() + " 集");

            checkPaused(runId);

            List<com.yaoai.domain.entity.Character> existingCharacters = characterMapper.findByProject(projectId, tenantId);
            List<Scene> existingScenes = sceneMapper.findByProject(projectId, tenantId);
            if (!existingCharacters.isEmpty() && !existingScenes.isEmpty()
                    && options.useExistingAssets() && !command.shouldForceRegenerate()) {
                skipStep(runId, "asset_gen", "已存在角色 " + existingCharacters.size() + " 个、场景 " + existingScenes.size() + " 个，按上下文策略跳过重提取");
            } else {
                if (!command.shouldForceRegenerate()
                        && (!existingCharacters.isEmpty() || !existingScenes.isEmpty())
                        && !options.useExistingAssets()) {
                    purgeAssets(runId, projectId, tenantId);
                }
                executeStep(runId, "asset_gen", () -> {
                sseManager.emitLog(runId, "agent", "[执行层] 提取角色和场景...");
                List<com.yaoai.domain.entity.Character> chars = assetGenPipeline.extractCharacters(projectId, tenantId);
                List<Scene> scenes = assetGenPipeline.extractScenes(projectId, tenantId);
                sseManager.emitLog(runId, "info",
                        "提取完成：" + chars.size() + " 个角色，" + scenes.size() + " 个场景");
                return "角色 " + chars.size() + " 个，场景 " + scenes.size() + " 个";
                });
            }

            checkPaused(runId);

            List<Storyboard> existingStoryboards = storyboardMapper.findByProject(projectId, tenantId);
            if (!existingStoryboards.isEmpty() && options.useExistingStoryboards() && !command.shouldForceRegenerate()) {
                skipStep(runId, "storyboard_gen", "已存在 " + existingStoryboards.size() + " 条分镜，按上下文策略跳过重生成");
            } else {
                if (!command.shouldForceRegenerate() && !existingStoryboards.isEmpty() && !options.useExistingStoryboards()) {
                    purgeStoryboards(runId, projectId, tenantId);
                }
                List<Episode> finalEpisodes = episodes;
                executeStep(runId, "storyboard_gen", () -> {
                sseManager.emitLog(runId, "agent", "[执行层] 生成分镜...");
                int total = 0;
                int promptTotal = 0;
                for (Episode ep : finalEpisodes) {
                    List<Storyboard> boards = storyboardPipeline.generateStoryboards(
                            ep.getId(), projectId, tenantId);
                    total += boards.size();
                    List<Storyboard> enriched = storyboardPipeline.populateMissingVideoPromptsByEpisode(
                            ep.getId(), tenantId);
                    long promptCount = enriched.stream()
                            .filter(sb -> sb.getVideoPrompt() != null && !sb.getVideoPrompt().isBlank())
                            .count();
                    promptTotal += (int) promptCount;
                    sseManager.emitLog(runId, "info",
                            "第 " + ep.getEpisodeNumber() + " 集视频提示词：" + promptCount + " 条已生成");
                    sseManager.emitLog(runId, "info",
                            "第 " + ep.getEpisodeNumber() + " 集分镜：" + boards.size() + " 个镜头");
                }
                return "共生成 " + total + " 个分镜，并补全 " + promptTotal + " 条视频提示词";
                });
            }

            checkPaused(runId);

            // Step 7: Final Supervision
            executeSupervisionStep(runId, "supervision_2", projectId, tenantId,
                    "终审：完整制作流程质量评估");

            if (isQuickVideo(command)) {
                executeQuickVideoSteps(runId, projectId, tenantId, userId);
            }

            // Done
            updateRunStatus(runId, "DONE");
            sseManager.emit(runId, "run.done", Map.of("runId", runId));
            log.info("Agent run completed: runId={}", runId);

        } catch (PausedException e) {
            log.info("Agent run paused by user: runId={}", runId);
        } catch (Exception e) {
            log.error("Agent run failed: runId={}", runId, e);
            updateRunStatus(runId, "FAILED");
            sseManager.emit(runId, "run.failed", Map.of("error", e.getMessage()));
        } finally {
            UserContext.clear();
        }
    }

    private Object executeStep(Long runId, String stepKey, Supplier<Object> action) {
        GraphStep step = findStep(runId, stepKey);
        long startMs = System.currentTimeMillis();

        step.setStatus("RUNNING");
        step.setStartedAt(LocalDateTime.now());
        graphStepMapper.updateById(step);
        sseManager.emit(runId, "step.start", Map.of("stepKey", stepKey));

        try {
            Object result = action.get();
            long durationMs = System.currentTimeMillis() - startMs;

            step.setStatus("DONE");
            step.setDoneAt(LocalDateTime.now());
            step.setDurationMs(durationMs);
            step.setOutput(toOutputString(result));
            graphStepMapper.updateById(step);

            sseManager.emit(runId, "step.done", Map.of(
                    "stepKey", stepKey,
                    "output", step.getOutput(),
                    "durationMs", durationMs
            ));
            return result;
        } catch (Exception e) {
            step.setStatus("FAILED");
            step.setDoneAt(LocalDateTime.now());
            step.setDurationMs(System.currentTimeMillis() - startMs);
            step.setOutput("失败：" + e.getMessage());
            graphStepMapper.updateById(step);

            sseManager.emit(runId, "step.failed", Map.of(
                    "stepKey", stepKey,
                    "error", e.getMessage()
            ));
            throw e;
        }
    }

    private void executeSupervisionStep(Long runId, String stepKey, Long projectId, Long tenantId, String context) {
        GraphStep step = findStep(runId, stepKey);
        long startMs = System.currentTimeMillis();

        step.setStatus("RUNNING");
        step.setStartedAt(LocalDateTime.now());
        graphStepMapper.updateById(step);
        sseManager.emit(runId, "step.start", Map.of("stepKey", stepKey));
        sseManager.emitLog(runId, "agent", "[监督层] 正在审核...");

        try {
            String reviewResult = llmService.chat(ChatRequest.builder()
                    .messages(List.of(
                            ChatMessage.system(SUPERVISION_SYSTEM),
                            ChatMessage.user(context)
                    )).build());

            String score = extractScore(reviewResult);
            long durationMs = System.currentTimeMillis() - startMs;

            step.setStatus("DONE");
            step.setScore(score);
            step.setScoreDetail(reviewResult);
            step.setDoneAt(LocalDateTime.now());
            step.setDurationMs(durationMs);
            graphStepMapper.updateById(step);

            sseManager.emit(runId, "step.done", Map.of(
                    "stepKey", stepKey,
                    "score", score,
                    "scoreDetail", reviewResult,
                    "durationMs", durationMs
            ));
            sseManager.emitLog(runId, "agent", "[监督层] 评分：" + score);
        } catch (Exception e) {
            step.setStatus("FAILED");
            step.setDoneAt(LocalDateTime.now());
            graphStepMapper.updateById(step);
            sseManager.emit(runId, "step.failed", Map.of("stepKey", stepKey, "error", e.getMessage()));
            throw e;
        }
    }

    // ─── Helpers ───────────────────────────────────────────────────────────────

    private void executeQuickVideoSteps(Long runId, Long projectId, Long tenantId, Long userId) {
        List<Storyboard> storyboards = storyboardMapper.findByProject(projectId, tenantId);
        if (storyboards.isEmpty()) {
            throw new BizException(ErrorCode.INVALID_PARAM, "极速出片需要先生成分镜");
        }

        executeStep(runId, "image_generation", () -> {
            long promptCount = storyboards.stream()
                    .filter(storyboard -> hasText(storyboard.getVideoPrompt()))
                    .count();
            long referenceCount = storyboards.stream()
                    .filter(storyboard -> !storyboardImageKeys(storyboard).isEmpty())
                    .count();
            sseManager.emitLog(runId, "agent", "[极速出片] 已准备 " + storyboards.size()
                    + " 条分镜，" + promptCount + " 条具备视频提示词，" + referenceCount + " 条具备参考图。");
            return "已准备 " + storyboards.size() + " 条分镜；无参考图的镜头将自动走文生图再生成视频";
        });

        @SuppressWarnings("unchecked")
        List<AiTask> submittedTasks = (List<AiTask>) executeStep(runId, "video_generation", () -> {
            List<AiTask> tasks = new ArrayList<>();
            for (Storyboard storyboard : storyboards) {
                checkPaused(runId);
                String videoPrompt = firstText(
                        storyboard.getVideoPrompt(),
                        storyboard.getMotionScript(),
                        storyboard.getDetailedDescription(),
                        storyboard.getShortDescription(),
                        "根据当前分镜生成一段短剧镜头视频");
                String imagePrompt = firstText(
                        storyboard.getStartFramePrompt(),
                        storyboard.getDetailedDescription(),
                        videoPrompt);
                AiTask task = videoTaskPipeline.submitTextToVideoAsync(
                        projectId,
                        tenantId,
                        userId,
                        storyboard.getEpisodeId(),
                        storyboard.getId(),
                        imagePrompt,
                        videoPrompt,
                        storyboardImageKeys(storyboard),
                        durationSeconds(storyboard)
                );
                tasks.add(task);
                sseManager.emitLog(runId, "info", "已提交分镜 " + storyboard.getId() + " 的视频任务：" + task.getId());
            }
            return tasks;
        });

        executeStep(runId, "video_composition", () -> {
            waitForVideoTasks(runId, tenantId, submittedTasks);
            LinkedHashSet<Long> orderedEpisodeIds = storyboards.stream()
                    .map(Storyboard::getEpisodeId)
                    .filter(Objects::nonNull)
                    .collect(LinkedHashSet::new, LinkedHashSet::add, LinkedHashSet::addAll);
            List<Long> episodeIds = new ArrayList<>(orderedEpisodeIds);
            if (episodeIds.isEmpty()) {
                return "视频任务已完成，但没有可合成的分集 ID";
            }

            List<AssemblyTask> assemblies = new ArrayList<>();
            for (Long episodeId : episodeIds) {
                checkPaused(runId);
                AssemblyTask assembly = assemblyPipeline.startAssembly(projectId, episodeId, tenantId);
                assemblies.add(assembly);
                sseManager.emitLog(runId, "info", "已启动分集 " + episodeId + " 合成任务：" + assembly.getId());
            }
            waitForAssemblyTasks(runId, assemblies);
            return "已完成 " + assemblies.size() + " 个分集合成任务";
        });
    }

    private void waitForVideoTasks(Long runId, Long tenantId, List<AiTask> tasks) {
        long deadline = System.currentTimeMillis() + QUICK_VIDEO_WAIT_TIMEOUT.toMillis();
        List<Long> taskIds = tasks.stream().map(AiTask::getId).toList();
        while (System.currentTimeMillis() < deadline) {
            checkPaused(runId);
            int succeeded = 0;
            int failed = 0;
            for (Long taskId : taskIds) {
                AiTask task = aiTaskMapper.selectById(taskId);
                if (task == null) {
                    failed++;
                    continue;
                }
                if (tenantId.equals(task.getTenantId())
                        && hasText(task.getExternalTaskId())
                        && ("submitted".equals(task.getStatus()) || "running".equals(task.getStatus()))) {
                    task = videoTaskPipeline.pollTaskStatus(taskId, tenantId);
                }
                if ("succeeded".equals(task.getStatus())) {
                    succeeded++;
                } else if ("failed".equals(task.getStatus())) {
                    failed++;
                }
            }

            sseManager.emitLog(runId, "info", "视频生成进度：" + succeeded + "/" + taskIds.size() + "，失败 " + failed);
            if (failed > 0) {
                throw new BizException(ErrorCode.INTERNAL_ERROR, "极速出片中有视频任务失败");
            }
            if (succeeded == taskIds.size()) {
                return;
            }
            sleepQuietly();
        }
        throw new BizException(ErrorCode.INTERNAL_ERROR, "极速出片视频生成等待超时");
    }

    private void waitForAssemblyTasks(Long runId, List<AssemblyTask> assemblies) {
        long deadline = System.currentTimeMillis() + QUICK_VIDEO_ASSEMBLY_TIMEOUT.toMillis();
        List<Long> taskIds = assemblies.stream().map(AssemblyTask::getId).toList();
        while (System.currentTimeMillis() < deadline) {
            checkPaused(runId);
            int succeeded = 0;
            int failed = 0;
            for (Long taskId : taskIds) {
                AssemblyTask task = assemblyTaskMapper.selectById(taskId);
                if (task == null) {
                    failed++;
                    continue;
                }
                if ("succeeded".equals(task.getStatus())) {
                    succeeded++;
                } else if ("failed".equals(task.getStatus())) {
                    failed++;
                }
            }

            sseManager.emitLog(runId, "info", "合成进度：" + succeeded + "/" + taskIds.size() + "，失败 " + failed);
            if (failed > 0) {
                throw new BizException(ErrorCode.INTERNAL_ERROR, "极速出片中有合成任务失败");
            }
            if (succeeded == taskIds.size()) {
                return;
            }
            sleepQuietly();
        }
        throw new BizException(ErrorCode.INTERNAL_ERROR, "极速出片合成等待超时");
    }

    private void sleepQuietly() {
        try {
            Thread.sleep(QUICK_VIDEO_POLL_INTERVAL.toMillis());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BizException(ErrorCode.INTERNAL_ERROR, "极速出片等待被中断");
        }
    }

    private GraphStep findStep(Long runId, String stepKey) {
        return graphStepMapper.findByRunId(runId).stream()
                .filter(s -> s.getStepKey().equals(stepKey))
                .findFirst()
                .orElseThrow(() -> new BizException(ErrorCode.NOT_FOUND, "Step not found: " + stepKey));
    }

    private boolean hasStep(Long runId, String stepKey) {
        return graphStepMapper.findByRunId(runId).stream()
                .anyMatch(s -> s.getStepKey().equals(stepKey));
    }

    private void skipStep(Long runId, String stepKey, String output) {
        if (!hasStep(runId, stepKey)) {
            return;
        }
        GraphStep step = findStep(runId, stepKey);
        step.setStatus("SKIPPED");
        step.setStartedAt(LocalDateTime.now());
        step.setDoneAt(LocalDateTime.now());
        step.setDurationMs(0L);
        step.setOutput(output);
        graphStepMapper.updateById(step);
        sseManager.emit(runId, "step.skipped", Map.of(
                "stepKey", stepKey,
                "output", output
        ));
    }

    private void purgeAllGeneratedContent(Long runId, Long projectId, Long tenantId) {
        int storyboards = deleteStoryboards(projectId, tenantId);
        int episodes = deleteEpisodes(projectId, tenantId);
        int assets = deleteAssets(projectId, tenantId);
        int outlines = deleteOutlines(projectId, tenantId);
        sseManager.emitLog(runId, "agent", "已清理旧生成内容：大纲 " + outlines
                + " 个，分集 " + episodes + " 集，角色/场景 " + assets + " 个，分镜 " + storyboards + " 条");
    }

    private void purgeEpisodesAndStoryboards(Long runId, Long projectId, Long tenantId) {
        int storyboards = deleteStoryboards(projectId, tenantId);
        int episodes = deleteEpisodes(projectId, tenantId);
        sseManager.emitLog(runId, "agent", "已清理旧分集与分镜：分集 " + episodes + " 集，分镜 " + storyboards + " 条");
    }

    private void purgeAssets(Long runId, Long projectId, Long tenantId) {
        int assets = deleteAssets(projectId, tenantId);
        sseManager.emitLog(runId, "agent", "已清理旧角色/场景：" + assets + " 个");
    }

    private void purgeStoryboards(Long runId, Long projectId, Long tenantId) {
        int storyboards = deleteStoryboards(projectId, tenantId);
        sseManager.emitLog(runId, "agent", "已清理旧分镜：" + storyboards + " 条");
    }

    private int deleteOutlines(Long projectId, Long tenantId) {
        return outlineMapper.delete(Wrappers.<Outline>lambdaQuery()
                .eq(Outline::getProjectId, projectId)
                .eq(Outline::getTenantId, tenantId));
    }

    private int deleteEpisodes(Long projectId, Long tenantId) {
        return episodeMapper.delete(Wrappers.<Episode>lambdaQuery()
                .eq(Episode::getProjectId, projectId)
                .eq(Episode::getTenantId, tenantId));
    }

    private int deleteAssets(Long projectId, Long tenantId) {
        int characters = characterMapper.delete(Wrappers.<com.yaoai.domain.entity.Character>lambdaQuery()
                .eq(com.yaoai.domain.entity.Character::getProjectId, projectId)
                .eq(com.yaoai.domain.entity.Character::getTenantId, tenantId));
        int scenes = sceneMapper.delete(Wrappers.<Scene>lambdaQuery()
                .eq(Scene::getProjectId, projectId)
                .eq(Scene::getTenantId, tenantId));
        return characters + scenes;
    }

    private int deleteStoryboards(Long projectId, Long tenantId) {
        return storyboardMapper.delete(Wrappers.<Storyboard>lambdaQuery()
                .eq(Storyboard::getProjectId, projectId)
                .eq(Storyboard::getTenantId, tenantId));
    }

    private AgentRunCommand normalize(AgentRunCommand command) {
        AgentRunCommand normalized = command == null ? new AgentRunCommand() : command;
        if (normalized.getGoal() == null || normalized.getGoal().isBlank()) {
            normalized.setGoal("请根据当前项目资料完成短剧制作流程");
        }
        if (normalized.getRunIntent() == null || normalized.getRunIntent().isBlank()) {
            normalized.setRunIntent("auto");
        }
        if (normalized.getForceRegenerate() == null) {
            normalized.setForceRegenerate(false);
        }
        if (normalized.getSelectedSteps() == null) {
            normalized.setSelectedSteps(new ArrayList<>());
        }
        if (normalized.getContextOptions() == null) {
            normalized.setContextOptions(new AgentRunCommand.ContextOptions());
        }
        return normalized;
    }

    private List<StepDef> planSteps(boolean hasScript, AgentRunCommand command) {
        List<StepDef> candidates = new ArrayList<>();
        if (hasScript) {
            candidates.add(SCRIPT_UNDERSTANDING_STEP);
            BASE_STEP_DEFS.stream()
                    .filter(step -> !"decision".equals(step.key()))
                    .forEach(candidates::add);
        } else {
            candidates.addAll(BASE_STEP_DEFS);
        }
        if (isQuickVideo(command)) {
            candidates.addAll(QUICK_VIDEO_STEP_DEFS);
        }

        return candidates;
    }

    private boolean isQuickVideo(AgentRunCommand command) {
        return command != null && QUICK_VIDEO_INTENT.equalsIgnoreCase(command.getRunIntent());
    }

    private String runMode(boolean hasScript, AgentRunCommand command) {
        if (isQuickVideo(command)) {
            return hasScript ? "quick_video_script_based" : "quick_video_goal_based";
        }
        return hasScript ? "script_based" : "goal_based";
    }

    private void persistRunPlan(Long runId, String runMode, List<StepDef> plannedSteps, AgentRunCommand command) {
        AgentRunPlan plan = new AgentRunPlan();
        plan.setRunId(runId);
        plan.setRunMode(runMode);
        plan.setReasoningSummary(runMode.equals("script_based")
                ? "检测到剧本资产，优先执行剧情理解与事实抽取，再驱动生成链路。"
                : "未检测到剧本资产，按用户目标进入标准创作链路。");
        plan.setPlannedStepsJson(toJson(plannedSteps.stream()
                .map(step -> Map.of(
                        "key", step.key(),
                        "name", step.name(),
                        "agentType", step.agentType()
                ))
                .toList()));
        plan.setSkippedStepsJson("[]");
        plan.setStepInputsJson(toJson(Map.of(
                "goal", command.getGoal(),
                "runIntent", command.getRunIntent(),
                "selectedSteps", command.getSelectedSteps()
        )));
        plan.setQualityRulesJson(toJson(List.of(
                "后续生成必须优先遵循剧情理解层抽取的事实",
                "角色、场景、风格、画面提示词不得自由改写主线设定",
                "已有资产默认复用，除非显式要求强制重生成"
        )));
        plan.setModelPolicyJson(toJson(Map.of(
                "temperature", "low",
                "mode", "evidence_first"
        )));
        plan.setContextOptionsJson(toJson(command.getContextOptions()));
        plan.setIdempotencyKey(command.getIdempotencyKey());
        plan.setPlanVersion("agent-dynamic-v1");
        plan.setCreatedAt(LocalDateTime.now());
        agentRunPlanMapper.insert(plan);
    }

    private String toJson(Object value) {
        try {
            return objectMapper.writeValueAsString(value);
        } catch (Exception e) {
            log.warn("Failed to serialize agent plan payload", e);
            return "{}";
        }
    }

    private List<String> storyboardImageKeys(Storyboard storyboard) {
        List<String> keys = new ArrayList<>();
        keys.addAll(parseStringList(storyboard.getCharacterImageKeys()));
        if (hasText(storyboard.getSceneImageKey())) {
            keys.add(storyboard.getSceneImageKey());
        }
        keys.addAll(parseStringList(storyboard.getPropImageKeys()));
        if (hasText(storyboard.getStyleImageKey())) {
            keys.add(storyboard.getStyleImageKey());
        }
        return keys.stream()
                .filter(this::hasText)
                .distinct()
                .toList();
    }

    private List<String> parseStringList(String json) {
        if (!hasText(json)) {
            return List.of();
        }
        try {
            return objectMapper.readValue(
                    json,
                    objectMapper.getTypeFactory().constructCollectionType(List.class, String.class)
            );
        } catch (Exception e) {
            log.warn("Failed to parse storyboard image key list: {}", e.getMessage());
            return List.of();
        }
    }

    private int durationSeconds(Storyboard storyboard) {
        Integer duration = storyboard.getDurationSeconds();
        if (duration == null || duration <= 0) {
            return 5;
        }
        return Math.min(Math.max(duration, 5), 30);
    }

    private String firstText(String... candidates) {
        for (String candidate : candidates) {
            if (hasText(candidate)) {
                return candidate.trim();
            }
        }
        return "";
    }

    private boolean hasText(String value) {
        return value != null && !value.isBlank();
    }

    private void updateRunStatus(Long runId, String status) {
        GraphRun run = new GraphRun();
        run.setId(runId);
        run.setStatus(status);
        graphRunMapper.updateById(run);
    }

    private void checkPaused(Long runId) {
        GraphRun run = graphRunMapper.selectById(runId);
        if (run != null && "PAUSED".equals(run.getStatus())) {
            throw new PausedException();
        }
    }

    private String extractScore(String reviewText) {
        if (reviewText == null) return "B";
        String upper = reviewText.toUpperCase();
        if (upper.contains("评分：A") || upper.contains("SCORE: A") || upper.startsWith("A")) return "A";
        if (upper.contains("评分：B") || upper.contains("SCORE: B") || upper.startsWith("B")) return "B";
        if (upper.contains("评分：C") || upper.contains("SCORE: C") || upper.startsWith("C")) return "C";
        if (upper.contains("评分：D") || upper.contains("SCORE: D") || upper.startsWith("D")) return "D";
        return "B";
    }

    private String toOutputString(Object result) {
        if (result == null) return "";
        if (result instanceof String s) return s;
        if (result instanceof ScriptUnderstanding su) return "剧情理解：" + su.getQualityStatus();
        if (result instanceof Outline o) return "大纲《" + o.getTitle() + "》共 " + o.getEpisodeCount() + " 集";
        if (result instanceof List<?> list) return "生成 " + list.size() + " 条记录";
        return result.toString();
    }

    private static class PausedException extends RuntimeException {}

    // ─── Prompts ───────────────────────────────────────────────────────────────

    private static final String DECISION_SYSTEM = """
            你是一位AI动漫短剧制作导演。分析用户的制作目标，输出简洁的执行计划（100字以内）。
            执行顺序固定为：大纲→分集→角色场景→分镜。只需确认理解目标即可，不要执行。
            """;

    private static final String SUPERVISION_SYSTEM = """
            你是一位专业的影视内容质量审核专家。对提交的内容进行质量评审，
            按照以下标准给出评分并说明理由（150字以内）：
            - 评分 A：内容优质，可直接进入下一阶段
            - 评分 B：内容良好，有少量改进空间
            - 评分 C：内容一般，建议修改后继续
            - 评分 D：内容较差，建议重做

            回复格式：先写"评分：X"，再写简短评价。
            """;
}
