package com.infoepoch.pms.agent.platform.chat.application;

import com.alibaba.cloud.ai.graph.NodeOutput;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.action.InterruptionMetadata;
import com.alibaba.cloud.ai.graph.agent.Agent;
import com.alibaba.cloud.ai.graph.checkpoint.BaseCheckpointSaver;
import com.alibaba.cloud.ai.graph.exception.GraphRunnerException;
import com.alibaba.cloud.ai.graph.streaming.OutputType;
import com.alibaba.cloud.ai.graph.streaming.StreamingOutput;
import com.infoepoch.pms.agent.platform.agent.application.AgentQueryService;
import com.infoepoch.pms.agent.platform.agent.application.AgentExecutionService;
import com.infoepoch.pms.agent.platform.chat.application.dto.*;
import com.infoepoch.pms.agent.platform.chat.checkpoint.DatabaseCheckpointStore;
import com.infoepoch.pms.agent.platform.chat.domain.*;
import com.infoepoch.pms.agent.platform.chat.domain.irepository.ConversationCriteria;
import com.infoepoch.pms.agent.platform.chat.domain.irepository.IConversationRepository;
import com.infoepoch.pms.agent.platform.chat.domain.irepository.ISessionRepository;
import com.infoepoch.pms.agent.platform.chat.domain.irepository.SessionCriteria;
import com.infoepoch.pms.agent.platform.chatbot.application.ChatbotQueryService;
import com.infoepoch.pms.agent.platform.chatbot.domain.Chatbot;
import com.infoepoch.pms.agent.platform.shared.exception.BusinessException;
import com.infoepoch.pms.agent.platform.shared.observation.ToolExecutionTraceBuffer;
import com.infoepoch.pms.agent.platform.shared.observation.ToolExecutionTraceEvent;
import com.infoepoch.pms.agent.platform.shared.response.ResultCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

/**
 * 对话核心服务。
 * <p>
 * 负责会话的创建、消息的发送与存储、对话历史查询。
 * 每次发送消息时都按数据库中的最新 chatbot 配置动态构建 ReactAgent，
 * 对话状态通过数据库 durable checkpoint 持久化保存，不再依赖会话级内存缓存。
 * </p>
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatService {

    /**
     * 自动判定审批意图时允许自动执行的最小置信度。
     */
    private static final double AUTO_DECISION_CONFIDENCE_THRESHOLD = 0.90D;
    /**
     * 审批动作类型：提交。
     */
    private static final String SUBMIT_ACTION_TYPE = "submit";

    private final ISessionRepository sessionRepository;
    private final IConversationRepository conversationRepository;
    private final ChatbotQueryService chatbotQueryService;
    private final AgentQueryService agentQueryService;
    private final AgentExecutionService agentExecutionService;
    private final DatabaseCheckpointStore checkpointStore;
    private final SessionTitleService sessionTitleService;
    private final ToolExecutionTraceBuffer toolExecutionTraceBuffer;
    private final ApprovalStateService approvalStateService;
    private final ApprovalIntentClassifier approvalIntentClassifier;

    /**
     * 创建新会话。
     *
     * @param req 创建请求（含 userId 和 chatbotId）
     * @return 会话信息响应
     */
    @Transactional
    public SessionResponse createSession(CreateSessionRequest req) {
        Chatbot chatbot = chatbotQueryService.findEnabledById(req.getChatbotId())
                .orElseThrow(() -> new BusinessException(ResultCode.CHATBOT_NOT_FOUND,
                        "聊天机器人 ID=" + req.getChatbotId() + " 不存在或已禁用"));
        Session session = new Session(req.getUserId(), chatbot);
        sessionRepository.insert(session);
        return new SessionResponse(session);
    }

    /**
     * 查询会话信息。
     *
     * @param sessionId 会话 ID
     * @return 会话信息响应
     * @throws BusinessException SESSION_NOT_FOUND 若会话不存在
     */
    @Transactional(readOnly = true)
    public SessionResponse getSession(String sessionId) {
        return new SessionResponse(attachSessionRelations(getSessionOrThrow(sessionId)));
    }

    /**
     * 删除指定会话，并清理与该会话关联的审批状态和 checkpoint 线程。
     *
     * @param sessionId 会话 ID
     */
    @Transactional
    public void deleteSession(String sessionId) {
        Session session = getSessionOrThrow(sessionId);
        List<Conversation> conversations = listConversationsBySessionId(sessionId);
        conversations.forEach(conversation -> conversationRepository.delete(conversation.getId()));
        approvalStateService.clearSessionState(sessionId, collectSessionRootThreadIds(session, conversations));
        sessionRepository.delete(session.getId());
    }

    @Transactional
    public SessionResponse markContextResetPending(String sessionId) {
        Session session = getSessionOrThrow(sessionId);
        assertSessionActive(session);
        if (approvalStateService.getPendingApproval(sessionId, session.getActiveThreadId()).isPending()) {
            throw new BusinessException(ResultCode.PARAM_ERROR, "当前存在待审批事项，无法清除上下文");
        }
        if (!session.isContextResetPending()) {
            session.markContextResetPending();
            sessionRepository.update(session);
        }
        return new SessionResponse(session);
    }

    @Transactional
    public SessionResponse clearContextResetPending(String sessionId) {
        Session session = getSessionOrThrow(sessionId);
        assertSessionActive(session);
        if (session.isContextResetPending()) {
            session.clearContextResetPending();
            sessionRepository.update(session);
        }
        return new SessionResponse(session);
    }

    /**
     * 查询用户的历史会话列表，按时间倒序排列。
     */
    @Transactional(readOnly = true)
    public List<SessionResponse> listSessions(String userId) {
        return listSessionsByUserId(userId)
                .stream()
                .map(this::attachSessionRelations)
                .map(SessionResponse::new)
                .toList();
    }

    /**
     * 发送用户消息，调用主 ReactAgent，保存双方消息并返回 AI 回复。
     *
     * @param sessionId 会话 ID
     * @param req       发送消息请求（含 content）
     * @return AI 回复响应（含 status）
     * @throws BusinessException SESSION_NOT_FOUND 若会话不存在；SESSION_ALREADY_CLOSED 若会话已关闭
     */
    @Transactional
    public MessageResponse sendMessage(String sessionId, SendMessageRequest req) {
        Session session = getSessionOrThrow(sessionId);
        assertSessionActive(session);
        String resolvedThreadId = resolveThreadIdForSend(session);

        // 保存用户消息
        Conversation userMsg = new Conversation(session, resolvedThreadId, Conversation.ROLE_USER,
                req.getContent(), null, null);
        conversationRepository.insert(userMsg);

        // 会话运行时始终读取最新 chatbot 配置，使 prompt、子 agent、工具与模型变更能立即生效。
        Chatbot chatbot = chatbotQueryService.findDetailById(session.getChatBotId())
                .orElseThrow(() -> new BusinessException(ResultCode.CHATBOT_NOT_FOUND,
                        "聊天机器人 ID=" + session.getChatBotId() + " 不存在"));
        String requestKey = UUID.randomUUID().toString();
        Agent mainAgent = agentExecutionService.buildChatbotAgent(chatbot, checkpointStore, requestKey);
        RunnableConfig config = RunnableConfig.builder()
                .threadId(resolvedThreadId)
                .build();

        String replyContent;
        try {
            replyContent = extractAssistantReply(mainAgent.invoke(req.getContent(), config));
        } catch (GraphRunnerException e) {
            log.error("Agent 调用失败，session={}", sessionId, e);
            throw new BusinessException(ResultCode.SYSTEM_ERROR, "AI 调用失败：" + e.getMessage());
        } finally {
            toolExecutionTraceBuffer.clear(requestKey);
        }

        // 保存 AI 回复
        Conversation assistantMsg = new Conversation(session, resolvedThreadId, Conversation.ROLE_ASSISTANT,
                replyContent, null, null);
        conversationRepository.insert(assistantMsg);

        // V1 简单 status 判断：根据回复内容特征推断
        String status = inferStatus(replyContent);

        log.debug("session={}, userMsg='{}', aiReply='{}'", sessionId, req.getContent(), replyContent);

        // 如果是该会话的第一组对话（用户1条 + AI 1条 = 2条），且还没有标题，触发异步标题生成
        if (session.getTitle() == null && countConversationsBySessionId(session.getId()) == 2) {
            sessionTitleService.generateTitleAsync(sessionId, req.getContent());
        }

        return new MessageResponse(replyContent, null, status);
    }

    /**
     * 发送用户消息，并以流式事件返回 AI 调用过程与最终回复。
     *
     * @param sessionId 会话 ID
     * @param req       发送消息请求（含 content）
     * @return 流式事件序列
     */
    public Flux<ChatStreamEventResponse> streamMessage(String sessionId, SendMessageRequest req) {
        Session session = getSessionOrThrow(sessionId);
        assertSessionActive(session);
        String resolvedThreadId = resolveThreadIdForSend(session);

        Conversation userMsg = new Conversation(session, resolvedThreadId, Conversation.ROLE_USER,
                req.getContent(), null, null);
        conversationRepository.insert(userMsg);

        PendingApprovalResponse pendingApproval = approvalStateService.getPendingApproval(sessionId, resolvedThreadId);
        if (pendingApproval.isPending()) {
            return handlePendingApprovalInput(sessionId, session, req, pendingApproval);
        }

        return continueStreamingMessage(sessionId, session, req, resolvedThreadId);
    }

    /**
     * 在无待审批阻塞时继续执行常规流式对话。
     *
     * @param sessionId 会话 ID
     * @param session   会话实体
     * @param req       发送消息请求
     * @return 流式事件序列
     */
    private Flux<ChatStreamEventResponse> continueStreamingMessage(String sessionId, Session session,
                                                                   SendMessageRequest req,
                                                                   String resolvedThreadId) {
        Chatbot chatbot = chatbotQueryService.findDetailById(session.getChatBotId())
                .orElseThrow(() -> new BusinessException(ResultCode.CHATBOT_NOT_FOUND,
                        "聊天机器人 ID=" + session.getChatBotId() + " 不存在"));
        String requestKey = UUID.randomUUID().toString();
        Agent mainAgent = agentExecutionService.buildChatbotAgent(chatbot, checkpointStore, requestKey);
        RunnableConfig config = RunnableConfig.builder()
                .threadId(resolvedThreadId)
                .build();

        long startTime = System.currentTimeMillis();
        // 模型流式输出按 chunk 追加到这里，等 finished 事件到达后再统一作为 assistant 消息持久化
        StringBuilder replyBuilder = new StringBuilder();
        AtomicBoolean approvalCaptured = new AtomicBoolean(false);
        log.info("开始流式调用 agent, sessionId={}, chatbotId={}, contentLength={}",
                sessionId, chatbot.getId(), req.getContent().length());

        try {
            return mainAgent.stream(req.getContent(), config)
                    .concatMap(output -> mapStreamOutput(sessionId, requestKey, session, resolvedThreadId, chatbot, req.getContent(), output,
                            replyBuilder, approvalCaptured,
                            startTime))
                    .doOnError(error -> log.error("agent 流调用失败, sessionId={}", sessionId, error))
                    .doFinally(signalType -> {
                        toolExecutionTraceBuffer.clear(requestKey);
                    })
                    .onErrorMap(GraphRunnerException.class,
                            e -> new BusinessException(ResultCode.SYSTEM_ERROR, "AI 调用失败：" + e.getMessage()))
                    .onErrorMap(e -> e instanceof BusinessException ? e
                            : new BusinessException(ResultCode.SYSTEM_ERROR, "AI 调用失败：" + e.getMessage()));
        } catch (GraphRunnerException e) {
            log.error("agent 流调用失败, sessionId={}", sessionId, e);
            throw new BusinessException(ResultCode.SYSTEM_ERROR, "AI 调用失败：" + e.getMessage());
        }
    }

    /**
     * 查询会话当前待审批事项。
     *
     * @param sessionId 会话 ID
     * @return 待审批响应
     */
    @Transactional(readOnly = true)
    public PendingApprovalResponse getPendingApproval(String sessionId) {
        Session session = getSessionOrThrow(sessionId);
        Chatbot chatbot = chatbotQueryService.findDetailById(session.getChatBotId())
                .orElseThrow(() -> new BusinessException(ResultCode.CHATBOT_NOT_FOUND,
                        "聊天机器人 ID=" + session.getChatBotId() + " 不存在"));
        return approvalStateService.getPendingApproval(sessionId, session.getActiveThreadId(), chatbot);
    }

    /**
     * 提交审批决策并流式返回恢复结果。
     *
     * @param sessionId 会话 ID
     * @param req       审批决策请求
     * @return 审批恢复流
     */
    public Flux<ChatStreamEventResponse> streamApprovalDecision(String sessionId, ApprovalDecisionRequest req) {
        Session session = getSessionOrThrow(sessionId);
        ApprovalStateService.ApprovalDecisionContext decisionContext =
                approvalStateService.streamApprovalDecision(sessionId, session.getActiveThreadId(),
                        req.getInterruptionId(), req.getDecision(), req.getEditedFields());
        return continueApprovalDecisionMessage(sessionId, session, decisionContext);
    }

    /**
     * 查询会话的完整对话历史，按时间正序排列。
     *
     * @param sessionId 会话 ID
     * @return 消息列表
     * @throws BusinessException SESSION_NOT_FOUND 若会话不存在
     */
    @Transactional(readOnly = true)
    public List<ConversationResponse> getMessages(String sessionId) {
        Session session = getSessionOrThrow(sessionId);
        return listConversationsBySessionId(sessionId)
                .stream()
                .map(item -> attachConversationRelations(item, session))
                .map(ConversationResponse::new)
                .toList();
    }

    /**
     * 按会话 ID 查询会话，不存在时抛出业务异常。
     *
     * @param sessionId 会话 ID
     * @return 会话实体
     */
    private Session getSessionOrThrow(String sessionId) {
        Session session = sessionRepository.selectById(sessionId);
        if (session == null) {
            throw new BusinessException(ResultCode.SESSION_NOT_FOUND,
                    "会话 ID=" + sessionId + " 不存在");
        }
        return session;
    }

    private Session attachSessionRelations(Session session) {
        if (session == null || session.getChatBotId() == null) {
            return session;
        }
        Chatbot chatbot = chatbotQueryService.findDetailById(session.getChatBotId())
                .orElseThrow(() -> new BusinessException(ResultCode.CHATBOT_NOT_FOUND,
                        "聊天机器人 ID=" + session.getChatBotId() + " 不存在"));
        session.setChatbot(chatbot);
        return session;
    }

    private Conversation attachConversationRelations(Conversation conversation, Session session) {
        conversation.setSession(attachSessionRelations(session));
        if (conversation.getAgentId() != null) {
            agentQueryService.findById(conversation.getAgentId()).ifPresent(conversation::setAgent);
        }
        return conversation;
    }

    /**
     * 根据 AI 回复内容推断对话状态（V1 简化实现）。
     * <ul>
     *   <li>包含"已提交"或"申请已成功提交" → completed</li>
     *   <li>包含"请问"、"请填写"或"请输入" → collecting</li>
     *   <li>其他 → chatting</li>
     * </ul>
     *
     * @param replyContent AI 回复文本
     * @return status 字符串
     */
    private String inferStatus(String replyContent) {
        if (replyContent.contains("已提交") || replyContent.contains("申请已成功提交")) {
            return "completed";
        }
        if (replyContent.contains("请问") || replyContent.contains("请填写") || replyContent.contains("请输入")) {
            return "collecting";
        }
        return "chatting";
    }

    /**
     * 将 Graph 节点输出映射为前端可消费的流式事件。
     *
     * @param sessionId        会话 ID
     * @param requestKey       本次流式请求标识
     * @param session          会话实体
     * @param chatbot          聊天机器人配置
     * @param userContent      用户输入文本
     * @param output           节点输出
     * @param replyBuilder     回复文本累加器
     * @param approvalCaptured 是否已捕获审批中断
     * @param startTime        流式处理开始时间戳
     * @return 映射后的流式事件序列
     */
    private Flux<ChatStreamEventResponse> mapStreamOutput(String sessionId, String requestKey, Session session,
                                                          String resolvedThreadId,
                                                          Chatbot chatbot,
                                                          String userContent,
                                                          NodeOutput output, StringBuilder replyBuilder,
                                                          AtomicBoolean approvalCaptured,
                                                          long startTime) {
        Flux<ChatStreamEventResponse> toolTraceEvents = Flux.fromIterable(
                toolExecutionTraceBuffer.drain(requestKey)
        ).map(ChatStreamEventResponse::fromToolTrace);

        if (approvalCaptured.get()) {
            return Flux.empty();
        }

        if (output instanceof InterruptionMetadata interruptionMetadata) {
            PendingApprovalResponse pendingApproval = approvalStateService.capturePendingApproval(
                    requestKey, sessionId, resolvedThreadId, interruptionMetadata, chatbot);
            if (pendingApproval.isPending()) {
                approvalCaptured.set(true);
                return toolTraceEvents.concatWith(Flux.just(ChatStreamEventResponse.approvalRequired(
                        SUBMIT_ACTION_TYPE,
                        sessionId,
                        pendingApproval.getInterruptionId(),
                        SUBMIT_ACTION_TYPE,
                        pendingApproval.getAgentName(),
                        pendingApproval.getAgentId(),
                        pendingApproval.getSummaryFields())));
            }
            return toolTraceEvents;
        }

        if (!(output instanceof StreamingOutput<?> streamingOutput)) {
            return toolTraceEvents;
        }

        OutputType outputType = streamingOutput.getOutputType();
        log.info("agent 流事件, sessionId={}, node={}, outputType={}", sessionId, output.node(), outputType);

        if (outputType == OutputType.AGENT_MODEL_STREAMING) {
            // 只向前端推送增量文本，不在 streaming 阶段写数据库，避免保存半截回复。
            String chunk = getMessageText(streamingOutput);
            replyBuilder.append(chunk);
            return toolTraceEvents.concatWith(Flux.just(new ChatStreamEventResponse("message", chunk, output.node(), null)));
        }

        if (outputType == OutputType.AGENT_TOOL_FINISHED) {
            return toolTraceEvents;
        }

        if (outputType == OutputType.AGENT_HOOK_FINISHED) {
            return toolTraceEvents.concatWith(Flux.just(new ChatStreamEventResponse("hook_finished", null, output.node(), null)));
        }

        if (outputType == OutputType.AGENT_MODEL_FINISHED) {
            if (approvalCaptured.get()) {
                return toolTraceEvents;
            }
            // finished 事件到达后再取最终回复并持久化到数据库，保证会话历史里只有完整 assistant 消息。
            String replyContent = getFinishedReplyContent(streamingOutput, replyBuilder);
            if (hasToolCalls(streamingOutput)) {
                log.info("模型返回工具调用指令，等待后续工具执行, sessionId={}, node={}, toolCalls={}",
                        sessionId, output.node(), streamingOutput.message());
                if (StringUtils.hasText(replyContent)) {
                    conversationRepository.insert(buildAssistantConversation(session, resolvedThreadId, replyContent,
                            toolExecutionTraceBuffer.snapshot(requestKey)));
                }
                return toolTraceEvents.concatWith(buildPendingToolCallMessageEvent(replyBuilder, replyContent, output.node()));
            }
            if (!StringUtils.hasText(replyContent)) {
                log.error("模型流结束但未返回有效内容, sessionId={}, node={}, message={}, chunk={}, originData={}",
                        sessionId, output.node(), streamingOutput.message(), streamingOutput.chunk(),
                        streamingOutput.getOriginData());
                throw new BusinessException(ResultCode.SYSTEM_ERROR, "AI 未返回有效内容");
            }
            conversationRepository.insert(buildAssistantConversation(session, resolvedThreadId, replyContent,
                    toolExecutionTraceBuffer.snapshot(requestKey)));
            String status = inferStatus(replyContent);

            log.info("agent 流结束, sessionId={}, replyLength={}, durationMs={}",
                    sessionId, replyContent.length(), System.currentTimeMillis() - startTime);

            if (StringUtils.hasText(userContent) && session.getTitle() == null && countConversationsBySessionId(session.getId()) == 2) {
                sessionTitleService.generateTitleAsync(sessionId, userContent);
            }

            return toolTraceEvents.concatWith(Flux.just(new ChatStreamEventResponse("complete", replyContent, output.node(), status)));
        }

        return toolTraceEvents;
    }

    /**
     * 当模型结束但存在 tool calls 时，补齐应发送给前端的增量文本事件。
     *
     * @param replyBuilder 已累计的流式文本
     * @param replyContent 本轮模型完整回复
     * @param nodeName     节点名称
     * @return 需要补发的 message 事件
     */
    private Flux<ChatStreamEventResponse> buildPendingToolCallMessageEvent(StringBuilder replyBuilder,
                                                                           String replyContent,
                                                                           String nodeName) {
        if (!StringUtils.hasText(replyContent)) {
            return Flux.empty();
        }
        String streamedContent = replyBuilder.toString();
        if (!StringUtils.hasText(streamedContent)) {
            replyBuilder.append(replyContent);
            return Flux.just(new ChatStreamEventResponse("message", replyContent, nodeName, null));
        }
        if (replyContent.startsWith(streamedContent)) {
            String delta = replyContent.substring(streamedContent.length());
            if (StringUtils.hasText(delta)) {
                replyBuilder.append(delta);
                return Flux.just(new ChatStreamEventResponse("message", delta, nodeName, null));
            }
            return Flux.empty();
        }
        if (!replyContent.equals(streamedContent)) {
            replyBuilder.setLength(0);
            replyBuilder.append(replyContent);
            return Flux.just(new ChatStreamEventResponse("message", replyContent, nodeName, null));
        }
        return Flux.empty();
    }

    /**
     * 在存在待审批事项时处理用户输入。
     *
     * @param sessionId       会话 ID
     * @param session         会话实体
     * @param req             发送消息请求
     * @param pendingApproval 当前待审批信息
     * @return 对应处理流程的流式事件序列
     */
    private Flux<ChatStreamEventResponse> handlePendingApprovalInput(String sessionId, Session session,
                                                                     SendMessageRequest req,
                                                                     PendingApprovalResponse pendingApproval) {
        ApprovalIntentClassifier.Result result = approvalIntentClassifier.classify(req.getContent());
        ApprovalIntent intent = result.intent();
        if (intent == ApprovalIntent.EDIT || intent == ApprovalIntent.REJECT_AND_EDIT) {
            silentlyRejectPendingApprovalForEdit(sessionId, session, pendingApproval);
            return continueStreamingMessage(sessionId, session, req, session.getActiveThreadId());
        }
        if (intent == ApprovalIntent.APPROVE || intent == ApprovalIntent.REJECT) {
            if (result.confidence() < AUTO_DECISION_CONFIDENCE_THRESHOLD) {
                return buildPendingApprovalGuidance(pendingApproval);
            }
            if (!StringUtils.hasText(pendingApproval.getInterruptionId())) {
                return buildPendingApprovalGuidance(pendingApproval);
            }
            ApprovalDecisionRequest decisionRequest = new ApprovalDecisionRequest();
            decisionRequest.setInterruptionId(pendingApproval.getInterruptionId());
            decisionRequest.setDecision(intent == ApprovalIntent.APPROVE ? "approve" : "reject");
            decisionRequest.setMessage(req.getContent());
            return streamApprovalDecision(sessionId, decisionRequest);
        }
        return buildPendingApprovalGuidance(pendingApproval);
    }

    /**
     * 当用户表达“修改”意图时，先静默拒绝当前审批，再继续普通对话收集新字段。
     *
     * @param sessionId       会话 ID
     * @param session         会话实体
     * @param pendingApproval 当前待审批信息
     */
    private void silentlyRejectPendingApprovalForEdit(String sessionId,
                                                      Session session,
                                                      PendingApprovalResponse pendingApproval) {
        if (!StringUtils.hasText(pendingApproval.getInterruptionId())) {
            approvalStateService.invalidatePendingApproval(sessionId);
            return;
        }
        ApprovalStateService.ApprovalDecisionContext decisionContext =
                approvalStateService.streamApprovalDecision(sessionId, session.getActiveThreadId(),
                        pendingApproval.getInterruptionId(), "reject", null);
        resumeApprovalDecisionSilently(session, decisionContext);
    }

    /**
     * 以非流式方式恢复一次审批决策，供“修改后重提”场景内部使用。
     *
     * @param session         会话实体
     * @param decisionContext 审批恢复上下文
     */
    private void resumeApprovalDecisionSilently(Session session,
                                                ApprovalStateService.ApprovalDecisionContext decisionContext) {
        Chatbot chatbot = chatbotQueryService.findDetailById(session.getChatBotId())
                .orElseThrow(() -> new BusinessException(ResultCode.CHATBOT_NOT_FOUND,
                        "聊天机器人 ID=" + session.getChatBotId() + " 不存在"));
        String requestKey = UUID.randomUUID().toString();
        Agent mainAgent = agentExecutionService.buildChatbotAgent(chatbot, checkpointStore, requestKey);
        RunnableConfig config = RunnableConfig.builder()
                .threadId(decisionContext.checkpointThreadId())
                .checkPointId(decisionContext.checkpointId())
                .resume()
                .addHumanFeedback(decisionContext.humanFeedback())
                .build();
        try {
            mainAgent.invoke(Map.of(), config);
        } catch (GraphRunnerException e) {
            log.error("静默恢复待审批失败, sessionId={}, checkpointThreadId={}",
                    session.getId(), decisionContext.checkpointThreadId(), e);
            throw new BusinessException(ResultCode.SYSTEM_ERROR, "处理申请修改失败：" + e.getMessage());
        } finally {
            toolExecutionTraceBuffer.clear(requestKey);
        }
    }

    /**
     * 按审批决策上下文恢复图执行，并将恢复过程以流式事件返回。
     *
     * @param sessionId       会话 ID
     * @param session         会话实体
     * @param decisionContext 审批恢复上下文
     * @return 恢复执行流式事件序列
     */
    private Flux<ChatStreamEventResponse> continueApprovalDecisionMessage(String sessionId,
                                                                          Session session,
                                                                          ApprovalStateService.ApprovalDecisionContext decisionContext) {
        Chatbot chatbot = chatbotQueryService.findDetailById(session.getChatBotId())
                .orElseThrow(() -> new BusinessException(ResultCode.CHATBOT_NOT_FOUND,
                        "聊天机器人 ID=" + session.getChatBotId() + " 不存在"));
        String requestKey = UUID.randomUUID().toString();
        Agent mainAgent = agentExecutionService.buildChatbotAgent(chatbot, checkpointStore, requestKey);
        RunnableConfig config = RunnableConfig.builder()
                .threadId(decisionContext.checkpointThreadId())
                .checkPointId(decisionContext.checkpointId())
                .resume()
                .addHumanFeedback(decisionContext.humanFeedback())
                .build();
        return streamAgentResponse(sessionId, requestKey, session, null, mainAgent, config, Map.of(), chatbot);
    }

    /**
     * 统一封装 Agent 流式调用与输出映射逻辑。
     *
     * @param sessionId   会话 ID
     * @param requestKey  本次流式请求标识
     * @param session     会话实体
     * @param userContent 用户输入文本
     * @param mainAgent   主 Agent
     * @param config      运行配置
     * @param input       流式输入（String 或 Map）
     * @param chatbot     聊天机器人配置
     * @return 流式事件序列
     */
    @SuppressWarnings("unchecked")
    private Flux<ChatStreamEventResponse> streamAgentResponse(String sessionId,
                                                              String requestKey,
                                                              Session session,
                                                              String userContent,
                                                              Agent mainAgent,
                                                              RunnableConfig config,
                                                              Object input,
                                                              Chatbot chatbot) {
        long startTime = System.currentTimeMillis();
        StringBuilder replyBuilder = new StringBuilder();
        AtomicBoolean approvalCaptured = new AtomicBoolean(false);
        try {
            Flux<NodeOutput> stream = input instanceof Map<?, ?> map
                    ? mainAgent.stream((Map<String, Object>) map, config)
                    : mainAgent.stream((String) input, config);
            return stream.concatMap(output -> mapStreamOutput(sessionId, requestKey, session, session.getActiveThreadId(),
                            chatbot, userContent, output,
                            replyBuilder, approvalCaptured, startTime))
                    .doOnError(error -> log.error("agent 流调用失败, sessionId={}", sessionId, error))
                    .doFinally(signalType -> toolExecutionTraceBuffer.clear(requestKey))
                    .onErrorMap(GraphRunnerException.class,
                            e -> new BusinessException(ResultCode.SYSTEM_ERROR, "AI 调用失败：" + e.getMessage()))
                    .onErrorMap(e -> e instanceof BusinessException ? e
                            : new BusinessException(ResultCode.SYSTEM_ERROR, "AI 调用失败：" + e.getMessage()));
        } catch (GraphRunnerException e) {
            log.error("agent 流调用失败, sessionId={}", sessionId, e);
            throw new BusinessException(ResultCode.SYSTEM_ERROR, "AI 调用失败：" + e.getMessage());
        }
    }

    /**
     * 构建“存在待审批事项”时的引导文案事件。
     *
     * @param pendingApproval 当前待审批信息
     * @return 引导消息事件
     */
    private Flux<ChatStreamEventResponse> buildPendingApprovalGuidance(PendingApprovalResponse pendingApproval) {
        String agentName = StringUtils.hasText(pendingApproval.getAgentName())
                ? pendingApproval.getAgentName()
                : "当前";
        String message = "当前有待审批的申请（" + agentName + "），请先同意、拒绝，或明确说明要修改哪些内容。";
        return Flux.just(new ChatStreamEventResponse("message", message, null, "collecting"));
    }

    /**
     * 获取模型完成阶段应落库的最终回复文本。
     *
     * @param streamingOutput 模型完成输出
     * @param replyBuilder    已累计的流式文本
     * @return 最终回复文本
     */
    private String getFinishedReplyContent(StreamingOutput<?> streamingOutput, StringBuilder replyBuilder) {
        // 优先使用 finished 事件给出的完整文本；若框架未携带，则回退到已累计的 chunk。
        String finalReply = getMessageText(streamingOutput);
        if (StringUtils.hasText(finalReply)) {
            return finalReply;
        }
        return replyBuilder.toString();
    }

    /**
     * 从流式输出中提取文本内容。
     *
     * @param streamingOutput 流式输出
     * @return 提取出的文本，提取失败时返回空串
     */
    private String getMessageText(StreamingOutput<?> streamingOutput) {
        if (streamingOutput.message() instanceof AssistantMessage assistantMessage) {
            return assistantMessage.getText();
        }
        if (streamingOutput.message() != null && streamingOutput.message().getText() != null) {
            return streamingOutput.message().getText();
        }
        if (StringUtils.hasText(streamingOutput.chunk())) {
            return streamingOutput.chunk();
        }
        return "";
    }

    /**
     * 判断本次模型输出是否包含工具调用指令。
     *
     * @param streamingOutput 流式输出
     * @return true 表示包含 tool calls
     */
    private boolean hasToolCalls(StreamingOutput<?> streamingOutput) {
        if (streamingOutput.message() instanceof AssistantMessage assistantMessage) {
            return assistantMessage.hasToolCalls();
        }
        return false;
    }

    /**
     * 从 invoke 返回状态中提取最后一条 assistant 文本回复。
     *
     * @param state invoke 返回状态
     * @return assistant 回复文本
     */
    private String extractAssistantReply(Optional<OverAllState> state) {
        Object messagesValue = state.flatMap(overAllState -> overAllState.value("messages"))
                .orElse(null);
        if (!(messagesValue instanceof List<?> messages)) {
            throw new BusinessException(ResultCode.SYSTEM_ERROR, "AI 未返回有效内容");
        }
        for (int i = messages.size() - 1; i >= 0; i--) {
            Object message = messages.get(i);
            if (message instanceof AssistantMessage assistantMessage && StringUtils.hasText(assistantMessage.getText())) {
                return assistantMessage.getText();
            }
        }
        throw new BusinessException(ResultCode.SYSTEM_ERROR, "AI 未返回有效内容");
    }

    /**
     * 组装 assistant 消息实体，包含 markdown 正文与可选轨迹时间线块。
     *
     * @param session      会话实体
     * @param replyContent 回复文本
     * @param traceEvents  工具轨迹事件列表
     * @return assistant 消息实体
     */
    private Conversation buildAssistantConversation(Session session, String threadId, String replyContent,
                                                    List<ToolExecutionTraceEvent> traceEvents) {
        List<ChatMessageBlock> blocks = new ArrayList<>();
        blocks.add(new MarkdownBlock(replyContent));

        List<TraceTimelineItem> traceItems = buildTraceTimelineItems(traceEvents);
        if (!traceItems.isEmpty()) {
            long failedCount = traceItems.stream()
                    .filter(item -> "failed".equals(item.getStatus()))
                    .count();
            TraceTimelineBlock.TraceTimelineSummary summary =
                    new TraceTimelineBlock.TraceTimelineSummary(traceItems.size(), failedCount);
            blocks.add(new TraceTimelineBlock(summary, traceItems));
        }

        return new Conversation(session, threadId, Conversation.ROLE_ASSISTANT, replyContent, null, null, blocks);
    }

    /**
     * 将工具轨迹事件归并为时间线条目列表。
     *
     * @param traceEvents 工具轨迹事件列表
     * @return 时间线条目列表（按首次出现顺序）
     */
    private List<TraceTimelineItem> buildTraceTimelineItems(List<ToolExecutionTraceEvent> traceEvents) {
        return new ArrayList<>(traceEvents.stream()
                .collect(Collectors.toMap(
                        ToolExecutionTraceEvent::getToolCallId,
                        TraceTimelineItem::fromEvent,
                        TraceTimelineItem::merge,
                        LinkedHashMap::new
                ))
                .values());
    }

    private void assertSessionActive(Session session) {
        if (!Session.STATUS_ACTIVE.equals(session.getStatus())) {
            throw new BusinessException(ResultCode.SESSION_ALREADY_CLOSED,
                    "会话已关闭，无法发送消息");
        }
    }

    private String resolveThreadIdForSend(Session session) {
        String activeThreadId = StringUtils.hasText(session.getActiveThreadId()) ? session.getActiveThreadId() : session.getId();
        if (!session.isContextResetPending()) {
            return activeThreadId;
        }
        String nextThreadId = session.getId() + "_thread_" + UUID.randomUUID();
        session.activateNextThread(nextThreadId);
        sessionRepository.update(session);
        return nextThreadId;
    }

    private List<Session> listSessionsByUserId(String userId) {
        SessionCriteria criteria = new SessionCriteria();
        criteria.setUserId(userId);
        List<Session> sessions = sessionRepository.selectByCriteria(criteria);
        if (sessions == null) {
            return List.of();
        }
        return sessions.stream()
                .sorted(Comparator.comparing(Session::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder())))
                .toList();
    }

    private List<Conversation> listConversationsBySessionId(String sessionId) {
        ConversationCriteria criteria = new ConversationCriteria();
        criteria.setSessionId(sessionId);
        List<Conversation> conversations = conversationRepository.selectByCriteria(criteria);
        if (conversations == null) {
            return List.of();
        }
        return conversations.stream()
                .sorted(Comparator.comparing(Conversation::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())))
                .toList();
    }

    private int countConversationsBySessionId(String sessionId) {
        ConversationCriteria criteria = new ConversationCriteria();
        criteria.setSessionId(sessionId);
        Integer count = conversationRepository.selectCountByCriteria(criteria);
        return count == null ? 0 : count;
    }

    private List<String> collectSessionRootThreadIds(Session session, List<Conversation> conversations) {
        LinkedHashSet<String> rootThreadIds = new LinkedHashSet<>();
        if (session != null && StringUtils.hasText(session.getId())) {
            rootThreadIds.add(resolveRootThreadId(session.getId()));
        }
        if (session != null && StringUtils.hasText(session.getActiveThreadId())) {
            rootThreadIds.add(resolveRootThreadId(session.getActiveThreadId()));
        }
        if (conversations != null) {
            conversations.stream()
                    .map(Conversation::getThreadId)
                    .filter(StringUtils::hasText)
                    .map(this::resolveRootThreadId)
                    .forEach(rootThreadIds::add);
        }
        return List.copyOf(rootThreadIds);
    }

    private String resolveRootThreadId(String threadId) {
        if (!StringUtils.hasText(threadId)) {
            return threadId;
        }
        int subgraphIndex = threadId.indexOf("_subgraph_");
        if (subgraphIndex < 0) {
            return threadId;
        }
        return threadId.substring(0, subgraphIndex);
    }
}
