package tech.easyflow.admin.controller.agent;

import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import io.agentscope.core.agui.model.RunAgentInput;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.agent.entity.AgentToolBinding;
import tech.easyflow.agent.publish.AgentPublishAppService;
import tech.easyflow.agent.runtime.AgentChatRequest;
import tech.easyflow.agent.runtime.AgentDraftChatRequest;
import tech.easyflow.agent.runtime.AgentRunService;
import tech.easyflow.agent.runtime.agui.AgentAguiHitlResolveRequest;
import tech.easyflow.agent.runtime.agui.AgentAguiRunStatusView;
import tech.easyflow.agent.runtime.composer.AgentComposerDraft;
import tech.easyflow.agent.runtime.composer.AgentComposerDraftService;
import tech.easyflow.agent.runtime.composer.AgentComposerSession;
import tech.easyflow.agent.runtime.document.AgentDocumentResource;
import tech.easyflow.agent.runtime.document.AgentDocumentService;
import tech.easyflow.agent.runtime.document.AgentDocumentUploadView;
import tech.easyflow.agent.runtime.media.AgentMediaService;
import tech.easyflow.agent.runtime.media.AgentMediaUploadView;
import com.easyagents.agent.runtime.media.AgentMediaResource;
import tech.easyflow.agent.security.AgentVisibilityQueryHelper;
import tech.easyflow.agent.service.AgentApprovalStateService;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.agent.service.AgentOptionQueryService;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.agent.service.AgentSkillBindingService;
import tech.easyflow.agent.service.AgentToolBindingService;
import tech.easyflow.agent.vo.AgentOptionView;
import tech.easyflow.agent.vo.AgentResourceOptionsView;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.log.annotation.LogReporterDisabled;

import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * Agent 管理端控制器。
 */
@RestController
@RequestMapping("/api/v1/agent")
public class AgentController extends BaseCurdController<AgentService, Agent> {

    @Resource
    private AgentToolBindingService agentToolBindingService;
    @Resource
    private AgentKnowledgeBindingService agentKnowledgeBindingService;
    @Resource
    private AgentSkillBindingService agentSkillBindingService;
    @Resource
    private AgentRunService agentRunService;
    @Resource
    private AgentPublishAppService agentPublishAppService;
    @Resource
    private AgentApprovalStateService agentApprovalStateService;
    @Resource
    private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
    @Resource
    private AgentMediaService agentMediaService;
    @Resource
    private AgentDocumentService agentDocumentService;
    @Resource
    private AgentComposerDraftService agentComposerDraftService;
    @Resource
    private AgentOptionQueryService agentOptionQueryService;
    @Resource
    private AgentVisibilityQueryHelper agentVisibilityQueryHelper;

    /**
     * 创建 Agent 控制器。
     *
     * @param service Agent 服务
     */
    public AgentController(AgentService service) {
        super(service);
    }

    /**
     * 获取 Agent 列表关键字搜索字段。
     *
     * @return Agent 名称和描述属性
     */
    @Override
    protected String[] getKeywordSearchProperties() {
        return new String[]{"name", "description"};
    }

    /**
     * 获取 Agent 详情。
     *
     * @param id Agent ID
     * @return Agent 详情
     */
    @GetMapping("/getDetail")
    public Result<AgentDetailView> getDetail(BigInteger id) {
        Agent agent = service.getDetail(id);
        agentApprovalStateService.fillAgentApprovalState(agent);
        aiResourceCreatorNameSupport.fillAgentCreatorNames(List.of(agent));
        return Result.ok(AgentDetailView.from(agent));
    }

    /**
     * 保存 Agent 草稿。
     *
     * @param agent Agent 草稿
     * @return Agent 详情
     */
    @Override
    @PostMapping("save")
    public Result<?> save(@JsonBody Agent agent) {
        Agent saved = service.saveDraft(agent);
        return Result.ok(AgentDetailView.from(service.getDetail(saved.getId())));
    }

    /**
     * 更新 Agent 草稿。
     *
     * @param agent Agent 草稿
     * @return Agent 详情
     */
    @Override
    @PostMapping("update")
    public Result<?> update(@JsonBody Agent agent) {
        Agent saved = service.updateDraft(agent);
        return Result.ok(AgentDetailView.from(service.getDetail(saved.getId())));
    }

    /**
     * 原子保存 Agent 草稿及本次发生变化的绑定组。
     *
     * @param request 设计器保存请求
     * @return 保存后的 Agent 与本次替换的绑定
     */
    @PostMapping("/draft/save")
    @SaCheckPermission("/api/v1/agent/save")
    public Result<AgentDetailView> saveDraft(@JsonBody(required = true, skipConvertError = false)
                                             AgentDraftSaveRequest request) {
        if (request == null || request.getAgent() == null) {
            throw new BusinessException("Agent 草稿不能为空");
        }
        Agent saved = service.saveDraftGraph(
                request.getAgent(),
                request.getToolBindings(),
                request.isReplaceToolBindings(),
                request.getKnowledgeBindings(),
                request.isReplaceKnowledgeBindings(),
                request.toSkillBindings(),
                request.isReplaceSkillBindings());
        return Result.ok(AgentDetailView.from(saved));
    }

    /**
     * 更新 Agent 可见范围。
     *
     * @param agent 包含 Agent ID 和可见范围的请求数据
     * @return 更新后的 Agent
     */
    @PostMapping("visibilityScope/update")
    @SaCheckPermission("/api/v1/agent/save")
    public Result<AgentDetailView> updateVisibilityScope(@JsonBody Agent agent) {
        return Result.ok(AgentDetailView.from(
                service.updateVisibilityScope(agent.getId(), agent.getVisibilityScope())));
    }

    /**
     * 查询 Agent 列表。
     *
     * @param entity 查询条件
     * @param asTree 是否转树
     * @param sortKey 排序字段
     * @param sortType 排序方式
     * @return Agent 列表
     */
    @Override
    public Result<List<Agent>> list(Agent entity, Boolean asTree, String sortKey, String sortType) {
        HttpServletRequest request = currentRequest();
        QueryWrapper queryWrapper = request == null ? QueryWrapper.create() : buildQueryWrapper(request);
        agentVisibilityQueryHelper.applyReadableAccess(queryWrapper);
        applyPublishedOnlyFilter(queryWrapper);
        queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
        List<Agent> agents = service.list(queryWrapper);
        if (isPublishedOnlyRequest()) {
            agents = agents.stream().map(agent -> service.fromSnapshot(agent.getPublishedSnapshotJson())).toList();
        }
        agents.forEach(this::sanitizeListItem);
        agentApprovalStateService.fillAgentApprovalState(agents);
        aiResourceCreatorNameSupport.fillAgentCreatorNames(agents);
        return Result.ok(agents);
    }

    /**
     * 查询 Agent 安全选择项。
     *
     * @param publishedOnly 是否仅返回已发布 Agent
     * @return Agent 安全选择项
     */
    @GetMapping("/options")
    @SaCheckPermission("/api/v1/agent/query")
    public Result<List<AgentOptionView>> options(
            @RequestParam(value = "publishedOnly", defaultValue = "false") boolean publishedOnly) {
        return Result.ok(agentOptionQueryService.listAgentOptions(publishedOnly));
    }

    /**
     * 查询 Agent 设计器的安全资源选项。
     *
     * @return 设计器资源选项
     */
    @GetMapping("/resourceOptions")
    @SaCheckPermission("/api/v1/agent/save")
    public Result<AgentResourceOptionsView> resourceOptions() {
        return Result.ok(agentOptionQueryService.listDesignerResourceOptions());
    }

    /**
     * 查询 Agent 会话可使用的知识库安全选项。
     *
     * @return 知识库选项
     */
    @GetMapping("/knowledgeOptions")
    @SaCheckPermission("/api/v1/agent/query")
    public Result<List<AgentResourceOptionsView.ResourceOption>> knowledgeOptions() {
        return Result.ok(agentOptionQueryService.listKnowledgeOptions());
    }

    /**
     * 查询指定 MCP 的安全工具列表。
     *
     * @param id MCP ID
     * @return MCP 工具列表
     */
    @GetMapping("/mcpToolOptions")
    @SaCheckPermission("/api/v1/agent/save")
    public Result<List<AgentResourceOptionsView.McpToolOption>> mcpToolOptions(
            @RequestParam BigInteger id) {
        return Result.ok(agentOptionQueryService.listMcpTools(id));
    }

    /**
     * 运行 Agent 纯文本聊天。
     *
     * @param request 聊天请求
     * @return SSE Emitter
     */
    @PostMapping("chat")
    @SaCheckPermission("/api/v1/agent/session/query")
    public SseEmitter chat(@JsonBody AgentChatRequest request) {
        return agentRunService.chat(request);
    }

    /**
     * 运行 Agent 草稿态纯文本试用。
     *
     * @param request 草稿试用请求
     * @return SSE Emitter
     */
    @PostMapping("/chat/draft")
    @SaCheckPermission("/api/v1/agent/save")
    public SseEmitter chatDraft(@JsonBody AgentDraftChatRequest request) {
        return agentRunService.chatDraft(request);
    }

    /**
     * 通过 AG-UI 协议运行正式 Agent 聊天。
     *
     * @param agentId URL 中的 Agent ID
     * @param input AG-UI 运行输入
     * @return 原生 AG-UI SSE
     */
    @PostMapping("/{agentId}/agui/run")
    @SaCheckPermission("/api/v1/agent/session/query")
    public SseEmitter chatAgui(@PathVariable BigInteger agentId,
                               @RequestBody RunAgentInput input) {
        return agentRunService.chatAgui(agentId, input);
    }

    /**
     * 通过 AG-UI 协议运行草稿 Agent 试用。
     *
     * @param input AG-UI 运行输入
     * @return 原生 AG-UI SSE
     */
    @PostMapping("/agui/run/draft")
    @SaCheckPermission("/api/v1/agent/save")
    public SseEmitter chatDraftAgui(@RequestBody RunAgentInput input) {
        return agentRunService.chatDraftAgui(input);
    }

    /**
     * 查询可重连 AG-UI 运行状态。
     *
     * @param runId 客户端运行 ID
     * @return 运行状态
     */
    @GetMapping("/agui/run/{runId}/status")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<AgentAguiRunStatusView> getAguiRunStatus(@PathVariable String runId) {
        return Result.ok(agentRunService.getAguiRunStatus(runId));
    }

    /**
     * 从指定游标继续订阅 AG-UI 运行事件。
     *
     * @param runId 客户端运行 ID
     * @param after 已消费的最后事件游标
     * @return 增量重放 SSE
     */
    @GetMapping("/agui/run/{runId}/events")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public SseEmitter subscribeAguiRun(@PathVariable String runId,
                                       @RequestParam(defaultValue = "0") long after) {
        return agentRunService.subscribeAguiRun(runId, after);
    }

    /**
     * 显式取消单次 AG-UI 运行。
     *
     * @param runId 客户端运行 ID
     * @return 操作结果
     */
    @PostMapping("/agui/run/{runId}/cancel")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<Void> cancelAguiRun(@PathVariable String runId) {
        agentRunService.cancelAguiRun(runId);
        return Result.ok();
    }

    /**
     * 处理 AG-UI 自定义 HITL 兼容桥审批。
     *
     * @param request 审批请求
     * @return 操作结果
     */
    @PostMapping("/agui/hitl/resolve")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<Void> resolveAguiApproval(@RequestBody AgentAguiHitlResolveRequest request) {
        agentRunService.resolveAguiApproval(request);
        return Result.ok();
    }

    /**
     * 上传一张 Agent 聊天临时图片。
     *
     * @param file 图片文件
     * @param mode 聊天模式
     * @param agentId Agent ID
     * @param sessionId 会话 ID
     * @return 上传结果
     */
    @PostMapping(value = "/media/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<AgentMediaUploadView> uploadMedia(@RequestParam("file") MultipartFile file,
                                                    @RequestParam("mode") String mode,
                                                    @RequestParam("agentId") String agentId,
                                                    @RequestParam("sessionId") String sessionId) {
        return Result.ok(agentMediaService.upload(file, mode, agentId, sessionId, SaTokenUtil.getLoginAccount()));
    }

    /**
     * 删除当前账号尚未发送的临时图片。
     *
     * @param uploadId 上传 ID
     * @return 操作结果
     */
    @PostMapping("/media/delete")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<Void> deleteMedia(@JsonBody(value = "uploadId", required = true) String uploadId) {
        agentMediaService.deleteUpload(uploadId, SaTokenUtil.getLoginAccount());
        return Result.ok();
    }

    /**
     * 通过鉴权代理读取 Agent 私有聊天图片。
     *
     * @param reference 稳定图片引用
     * @return 图片响应
     */
    @GetMapping("/media/content")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    @LogReporterDisabled
    public ResponseEntity<byte[]> mediaContent(@RequestParam("reference") String reference) {
        AgentMediaResource resource = agentMediaService.load(reference, SaTokenUtil.getLoginAccount());
        return ResponseEntity.ok()
                .header(HttpHeaders.CACHE_CONTROL, "private, no-store")
                .header(HttpHeaders.CONTENT_DISPOSITION, "inline")
                .contentType(MediaType.parseMediaType(resource.mimeType()))
                .contentLength(resource.bytes().length)
                .body(resource.bytes());
    }

    /**
     * 上传一份 Agent 聊天文档并异步触发轻量读取。
     *
     * @param file 文档文件
     * @param mode 聊天模式
     * @param agentId Agent ID
     * @param sessionId 会话 ID
     * @param uploadId 客户端生成的幂等上传 ID
     * @return 上传与读取状态
     */
    @PostMapping(value = "/media/document/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<AgentDocumentUploadView> uploadDocument(@RequestParam("file") MultipartFile file,
                                                          @RequestParam("mode") String mode,
                                                          @RequestParam("agentId") String agentId,
                                                          @RequestParam("sessionId") String sessionId,
                                                          @RequestParam(value = "uploadId", required = false)
                                                          String uploadId) {
        return Result.ok(agentDocumentService.upload(
                file, mode, agentId, sessionId, uploadId, SaTokenUtil.getLoginAccount()));
    }

    /**
     * 查询当前账号一个上传文档的读取状态。
     *
     * @param uploadId 上传 ID
     * @return 最新状态
     */
    @GetMapping("/media/document/status")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<AgentDocumentUploadView> documentStatus(@RequestParam("uploadId") String uploadId) {
        return Result.ok(agentDocumentService.status(uploadId, SaTokenUtil.getLoginAccount()));
    }

    /**
     * 重试一次明确失败的文档读取。
     *
     * @param uploadId 上传 ID
     * @return 重试后的状态
     */
    @PostMapping("/media/document/retry")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<AgentDocumentUploadView> retryDocument(
            @JsonBody(value = "uploadId", required = true) String uploadId) {
        return Result.ok(agentDocumentService.retry(uploadId, SaTokenUtil.getLoginAccount()));
    }

    /**
     * 删除当前账号尚未发送的临时文档。
     *
     * @param uploadId 上传 ID
     * @return 操作结果
     */
    @PostMapping("/media/document/delete")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<Void> deleteDocument(
            @JsonBody(value = "uploadId", required = true) String uploadId) {
        agentDocumentService.deleteUpload(uploadId, SaTokenUtil.getLoginAccount());
        return Result.ok();
    }

    /**
     * 通过鉴权代理流式下载 Agent 私有聊天文档。
     *
     * @param reference 稳定文档引用
     * @return 文档流
     */
    @GetMapping("/media/document/content")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    @LogReporterDisabled
    public ResponseEntity<StreamingResponseBody> documentContent(
            @RequestParam("reference") String reference) {
        AgentDocumentResource resource = agentDocumentService.load(
                reference, SaTokenUtil.getLoginAccount());
        StreamingResponseBody body = output -> {
            try (var input = resource.inputStream()) {
                input.transferTo(output);
            }
        };
        ContentDisposition disposition = ContentDisposition.attachment()
                .filename(resource.name(), StandardCharsets.UTF_8)
                .build();
        return ResponseEntity.ok()
                .header(HttpHeaders.CACHE_CONTROL, "private, no-store")
                .header(HttpHeaders.CONTENT_DISPOSITION, disposition.toString())
                .contentType(MediaType.parseMediaType(resource.mimeType()))
                .contentLength(resource.size())
                .body(body);
    }

    /**
     * 为输入框预分配稳定会话 ID。
     *
     * @param mode 聊天模式
     * @return 会话信息
     */
    @PostMapping("/composer/session")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<AgentComposerSession> allocateComposerSession(
            @JsonBody(value = "mode", required = true) String mode) {
        return Result.ok(agentComposerDraftService.allocateSession(mode));
    }

    /**
     * 保存 Agent 输入草稿。
     *
     * @param draft 输入草稿
     * @return 保存后的草稿
     */
    @PostMapping("/composer/draft/persist")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<AgentComposerDraft> saveComposerDraft(@JsonBody AgentComposerDraft draft) {
        return Result.ok(agentComposerDraftService.save(draft, SaTokenUtil.getLoginAccount()));
    }

    /**
     * 获取当前会话或最近未发送会话的输入草稿。
     *
     * @param mode 聊天模式
     * @param agentId Agent ID
     * @param sessionId 会话 ID，可为空
     * @return 输入草稿
     */
    @GetMapping("/composer/draft")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<AgentComposerDraft> getComposerDraft(@RequestParam("mode") String mode,
                                                       @RequestParam("agentId") String agentId,
                                                       @RequestParam(value = "sessionId", required = false) String sessionId) {
        return Result.ok(agentComposerDraftService.get(mode, agentId, sessionId, SaTokenUtil.getLoginAccount())
                .orElse(null));
    }

    /**
     * 删除已发送或主动清空的输入草稿。
     *
     * @param mode 聊天模式
     * @param agentId Agent ID
     * @param sessionId 会话 ID
     * @param imageUploadIds 调用方仍持有的上传 ID
     * @param documentUploadIds 调用方仍持有的文档上传 ID
     * @param deleteUploads 是否同时删除临时附件
     * @return 操作结果
     */
    @PostMapping("/composer/draft/delete")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<Void> deleteComposerDraft(@JsonBody(value = "mode", required = true) String mode,
                                            @JsonBody(value = "agentId", required = true) String agentId,
                                            @JsonBody(value = "sessionId", required = true) String sessionId,
                                            @JsonBody(value = "imageUploadIds") List<String> imageUploadIds,
                                            @JsonBody(value = "documentUploadIds") List<String> documentUploadIds,
                                            @JsonBody(value = "deleteUploads") Boolean deleteUploads) {
        agentComposerDraftService.delete(mode, agentId, sessionId, imageUploadIds, documentUploadIds,
                !Boolean.FALSE.equals(deleteUploads), SaTokenUtil.getLoginAccount());
        return Result.ok();
    }

    /**
     * 清理 Agent 草稿试运行会话。
     *
     * @param sessionId 草稿试运行会话 ID
     * @return 操作结果
     */
    @PostMapping("/chat/draft/clear")
    @SaCheckPermission("/api/v1/agent/save")
    public Result<Void> clearDraftSession(@JsonBody(value = "sessionId", required = true) String sessionId) {
        agentRunService.clearDraftSession(sessionId);
        return Result.ok();
    }

    /**
     * 批准工具执行。
     *
     * @param requestId 请求 ID
     * @param resumeToken 恢复令牌
     * @return 操作结果
     */
    @PostMapping("/run/approve")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<Void> approve(@JsonBody("requestId") String requestId,
                                @JsonBody(value = "resumeToken", required = true) String resumeToken) {
        agentRunService.approve(requestId, resumeToken);
        return Result.ok();
    }

    /**
     * 拒绝工具执行。
     *
     * @param requestId 请求 ID
     * @param resumeToken 恢复令牌
     * @param reason 拒绝原因
     * @return 操作结果
     */
    @PostMapping("/run/reject")
    @SaCheckPermission(value = {
            "/api/v1/agent/session/query", "/api/v1/agent/save"
    }, mode = SaMode.OR)
    public Result<Void> reject(@JsonBody("requestId") String requestId,
                               @JsonBody(value = "resumeToken", required = true) String resumeToken,
                               @JsonBody("reason") String reason) {
        agentRunService.reject(requestId, resumeToken, reason);
        return Result.ok();
    }

    /**
     * 更新 Agent 工具绑定。
     *
     * @param agentId Agent ID
     * @param bindings 工具绑定
     * @return 保存后的启用绑定
     */
    @PostMapping("/toolBinding/update")
    @SaCheckPermission("/api/v1/agent/save")
    public Result<List<AgentToolBinding>> updateToolBinding(@JsonBody(value = "agentId", required = true) BigInteger agentId,
                                                            @JsonBody("bindings") List<AgentToolBinding> bindings) {
        return Result.ok(agentToolBindingService.replaceBindings(agentId, bindings));
    }

    /**
     * 更新 Agent 知识库绑定。
     *
     * @param agentId Agent ID
     * @param bindings 知识库绑定
     * @return 保存后的启用绑定
     */
    @PostMapping("/knowledgeBinding/update")
    @SaCheckPermission("/api/v1/agent/save")
    public Result<List<AgentKnowledgeBinding>> updateKnowledgeBinding(@JsonBody(value = "agentId", required = true) BigInteger agentId,
                                                                      @JsonBody("bindings") List<AgentKnowledgeBinding> bindings) {
        return Result.ok(agentKnowledgeBindingService.replaceBindings(agentId, bindings));
    }

    /**
     * 原子替换 Agent 的全部 Skill 草稿绑定。
     *
     * @param request 白名单 Skill 引用请求
     * @return 服务端生成的安全 Skill 摘要
     */
    @PostMapping("/skillBinding/update")
    @SaCheckPermission("/api/v1/agent/save")
    public Result<List<AgentDetailView.SkillBindingView>> updateSkillBinding(
            @JsonBody(required = true, skipConvertError = false) AgentSkillBindingUpdateRequest request) {
        if (request == null || request.getAgentId() == null) {
            throw new BusinessException("Agent ID 不能为空");
        }
        List<tech.easyflow.agent.entity.AgentSkillBinding> bindings = request.getBindings() == null
                ? List.of()
                : request.getBindings().stream().map(AgentSkillBindingUpdateRequest.Binding::toEntity).toList();
        return Result.ok(agentSkillBindingService.replaceBindings(request.getAgentId(), bindings)
                .stream().map(AgentDetailView.SkillBindingView::from).toList());
    }

    /**
     * 提交发布审批。
     *
     * @param id Agent ID
     * @return 审批实例 ID
     */
    @PostMapping("/submitPublishApproval")
    @SaCheckPermission("/api/v1/agent/save")
    public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
        return buildApprovalActionResult(agentPublishAppService.submitPublishApproval(id), "已提交发布审批", "已直接发布");
    }

    /**
     * 提交下线审批。
     *
     * @param id Agent ID
     * @return 审批实例 ID
     */
    @PostMapping("/submitOfflineApproval")
    @SaCheckPermission("/api/v1/agent/save")
    public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
        return buildApprovalActionResult(agentPublishAppService.submitOfflineApproval(id), "已提交下线审批", "已直接下线");
    }

    /**
     * 提交删除审批。
     *
     * @param id Agent ID
     * @return 审批实例 ID
     */
    @PostMapping("/submitDeleteApproval")
    @SaCheckPermission("/api/v1/agent/remove")
    public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
        return buildApprovalActionResult(agentPublishAppService.submitDeleteApproval(id), "已提交删除审批", "已直接删除");
    }

    @Override
    public Result<?> remove(Serializable id) {
        throw new BusinessException("Agent 仅支持通过生命周期审批删除");
    }

    @Override
    public Result<?> removeBatch(Collection<Serializable> ids) {
        throw new BusinessException("Agent 仅支持通过生命周期审批删除");
    }

    /**
     * 查询 Agent 分页。
     *
     * @param page 分页参数
     * @param queryWrapper 查询条件
     * @return Agent 分页
     */
    @Override
    protected Page<Agent> queryPage(Page<Agent> page, QueryWrapper queryWrapper) {
        agentVisibilityQueryHelper.applyReadableAccess(queryWrapper);
        applyPublishedOnlyFilter(queryWrapper);
        Page<Agent> result = service.page(page, queryWrapper);
        if (isPublishedOnlyRequest()) {
            result.setRecords(result.getRecords().stream().map(agent -> service.fromSnapshot(agent.getPublishedSnapshotJson())).toList());
        }
        result.getRecords().forEach(this::sanitizeListItem);
        agentApprovalStateService.fillAgentApprovalState(result.getRecords());
        aiResourceCreatorNameSupport.fillAgentCreatorNames(result.getRecords());
        return result;
    }

    /**
     * 清理列表无需返回的配置和发布快照，避免敏感运行配置进入浏览器。
     *
     * @param agent Agent 列表项
     */
    private void sanitizeListItem(Agent agent) {
        if (agent == null) {
            return;
        }
        agent.setModelConfigJson(Collections.emptyMap());
        agent.setGenerationConfigJson(Collections.emptyMap());
        agent.setPromptConfigJson(Collections.emptyMap());
        agent.setMemoryConfigJson(Collections.emptyMap());
        agent.setExecutionConfigJson(Collections.emptyMap());
        agent.setInteractionConfigJson(Collections.emptyMap());
        agent.setPublishedSnapshotJson(Collections.emptyMap());
        agent.setToolBindings(null);
        agent.setKnowledgeBindings(null);
        agent.setSkillBindings(null);
    }

    /**
     * 为仅发布查询追加发布状态条件。
     *
     * @param queryWrapper Agent 查询条件
     */
    private void applyPublishedOnlyFilter(QueryWrapper queryWrapper) {
        if (isPublishedOnlyRequest()) {
            queryWrapper.eq("publish_status", PublishStatus.PUBLISHED.getCode());
        }
    }

    /**
     * 判断当前请求是否只查询已发布 Agent。
     *
     * @return 是否仅查询已发布 Agent
     */
    private boolean isPublishedOnlyRequest() {
        HttpServletRequest request = currentRequest();
        if (request == null) {
            return false;
        }
        return "true".equalsIgnoreCase(request.getParameter("publishedOnly"));
    }

    /**
     * 获取当前 HTTP 请求。
     *
     * @return 当前请求，不在 Web 请求上下文中时返回 null
     */
    private HttpServletRequest currentRequest() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return null;
        }
        return attributes.getRequest();
    }

    /**
     * 将审批执行结果转换为统一响应。
     *
     * @param actionResult 审批动作结果
     * @param approvalMessage 进入审批时的提示
     * @param directMessage 直接执行时的提示
     * @return 审批实例响应
     */
    private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
                                                         String approvalMessage,
                                                         String directMessage) {
        return Result.ok(actionResult.isApprovalRequired() ? approvalMessage : directMessage, actionResult.getInstanceId());
    }
}
