package com.easyagents.agent.runtime.event.interceptor;

import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
import com.easyagents.agent.runtime.AgentRuntimeException;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
import com.easyagents.agent.runtime.event.AgentRuntimeInterceptor;
import com.easyagents.agent.runtime.hitl.AgentPendingState;
import com.easyagents.agent.runtime.hitl.AgentToolApprovalEvaluation;
import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator;
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
import com.easyagents.agent.runtime.tool.AgentToolSpec;
import io.agentscope.core.hook.HookEvent;
import io.agentscope.core.hook.PostReasoningEvent;
import io.agentscope.core.hook.PreActingEvent;
import io.agentscope.core.message.ContentBlock;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.ToolUseBlock;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 工具 HITL 主线路干预器。
 *
 * <p>本 interceptor 专门处理“工具执行前人工审批”。通过 AgentScope 原生
 * {@link PostReasoningEvent} 建立审批批次，并在 {@link PreActingEvent} 消费一次性执行授权。</p>
 *
 * <p>这里包含三类动作：
 * <ul>
 *   <li>主线路干预：发现待审批工具后调用 {@link PostReasoningEvent#stopAgent()}，
 *   让 AgentScope 返回当前带 ToolUseBlock 的消息并暂停工具执行。</li>
 *   <li>执行前校验：按工具调用身份消费一次性执行授权，阻止未批准或被篡改的调用。</li>
 *   <li>旁路交互事件：通过 {@link AgentRuntimeEventBridge} 发出
 *   {@link AgentRuntimeEventType#TOOL_APPROVAL_REQUIRED}，通知调用方展示审批交互。</li>
 * </ul>
 *
 * <p>注意：本 interceptor 不执行工具。后续 resume 流程应基于 AgentScope pending tool
 * 状态继续调用 agent stream/call，实际工具执行仍由 AgentScope Toolkit 完成。</p>
 */
public class ToolHitlInterceptor implements AgentRuntimeInterceptor {

    private final AgentRuntimeEventBridge eventBridge;
    private final AgentToolApprovalCoordinator approvalCoordinator;
    private final Map<String, AgentToolSpec> toolSpecs;

    /**
     * 创建工具 HITL 干预器。
     *
     * @param eventBridge 旁路事件桥
     * @param approvalCoordinator 工具审批协调器
     * @param toolSpecs 工具声明列表
     */
    public ToolHitlInterceptor(AgentRuntimeEventBridge eventBridge,
                               AgentToolApprovalCoordinator approvalCoordinator,
                               List<AgentToolSpec> toolSpecs) {
        this.eventBridge = eventBridge;
        this.approvalCoordinator = approvalCoordinator;
        this.toolSpecs = (toolSpecs == null ? List.<AgentToolSpec>of() : toolSpecs).stream()
                .filter(spec -> spec != null && spec.getName() != null && !spec.getName().isBlank())
                .collect(Collectors.toMap(AgentToolSpec::getName, Function.identity(), (left, right) -> left,
                        LinkedHashMap::new));
    }

    /**
     * 处理 AgentScope Hook 事件。
     *
     * @param event AgentScope Hook 事件
     * @param <T> Hook 事件类型
     * @return 处理后的 Hook 事件
     */
    @Override
    public <T extends HookEvent> Mono<T> intercept(T event) {
        if (event instanceof PostReasoningEvent postReasoningEvent) {
            interceptPostReasoning(postReasoningEvent);
        } else if (event instanceof PreActingEvent preActingEvent) {
            interceptPreActing(preActingEvent);
        }
        return Mono.just(event);
    }

    /**
     * 返回执行优先级。
     *
     * <p>工具审批需要在普通观察器发出 reasoning completed 后保持事件已被标记暂停，
     * 但不应早于 AutoContext 的 PreReasoning 干预。当前值用于主线路 reasoning 后检查。</p>
     *
     * @return 优先级
     */
    @Override
    public int priority() {
        return 50;
    }

    /**
     * 归一化待审批调用、创建审批批次并暂停 AgentScope。
     *
     * @param event 推理完成事件
     */
    private void interceptPostReasoning(PostReasoningEvent event) {
        Msg reasoningMessage = normalizeApprovalToolUses(event.getReasoningMessage());
        if (reasoningMessage == null) {
            return;
        }
        if (reasoningMessage != event.getReasoningMessage()) {
            event.setReasoningMessage(reasoningMessage);
        }
        List<ToolUseBlock> approvalRequiredTools = approvalRequiredTools(reasoningMessage);
        if (approvalRequiredTools.isEmpty()) {
            return;
        }
        String approvalBatchId = approvalBatchId(reasoningMessage);
        for (ToolUseBlock toolUse : approvalRequiredTools) {
            AgentToolSpec toolSpec = toolSpecs.get(toolUse.getName());
            AgentPendingState pendingState = registerPendingState(toolSpec, toolUse, approvalBatchId);
            if (pendingState.getEventId() != null && !pendingState.getEventId().isBlank()) {
                continue;
            }
            AgentRuntimeEvent approvalEvent = toolApprovalRequiredEvent(toolSpec, toolUse, pendingState);
            pendingState.setEventId(approvalEvent.getEventId());
            eventBridge.emit(approvalEvent);
        }
        event.stopAgent();
    }

    /**
     * 在工具实际执行前消费与调用身份绑定的一次性授权。
     *
     * @param event 工具执行前事件
     */
    private void interceptPreActing(PreActingEvent event) {
        ToolUseBlock toolUse = event.getToolUse();
        AgentToolSpec toolSpec = toolUse == null ? null : toolSpecs.get(toolUse.getName());
        if (!requiresApproval(toolSpec, toolUse)) {
            return;
        }
        Map<String, Object> approvalMetadata = approvalMetadata(toolSpec, toolUse);
        if (!requiresForcedApproval(toolSpec, toolUse)
                && approvalCoordinator.isReusableApprovalGranted(approvalMetadata)) {
            return;
        }
        // 执行授权与 toolCallId、工具名称及入参同时绑定，并且只能消费一次。
        approvalCoordinator.consumeExecutionAuthorization(
                toolUse.getId(),
                toolUse.getName(),
                toolUse.getInput());
    }

    /**
     * 为缺少ID的审批调用补充稳定ID，并按 toolCallId 去除同轮重放调用。
     *
     * @param reasoningMessage 原始推理消息
     * @return 归一化后的推理消息
     */
    private Msg normalizeApprovalToolUses(Msg reasoningMessage) {
        if (reasoningMessage == null || reasoningMessage.getContent() == null
                || reasoningMessage.getContent().isEmpty()) {
            return reasoningMessage;
        }
        List<ContentBlock> normalizedContent = new ArrayList<>(reasoningMessage.getContent().size());
        Map<String, ToolCallSignature> seenApprovalCalls = new LinkedHashMap<>();
        boolean changed = false;
        for (int contentIndex = 0; contentIndex < reasoningMessage.getContent().size(); contentIndex++) {
            ContentBlock block = reasoningMessage.getContent().get(contentIndex);
            if (!(block instanceof ToolUseBlock toolUse) || !isApprovalRequired(toolUse)) {
                normalizedContent.add(block);
                continue;
            }
            ToolUseBlock normalizedToolUse = toolUse;
            if (toolUse.getId() == null || toolUse.getId().isBlank()) {
                normalizedToolUse = copyWithId(toolUse, stableToolCallId(reasoningMessage, contentIndex));
                changed = true;
            }
            ToolCallSignature signature = new ToolCallSignature(
                    normalizedToolUse.getName(), normalizedToolUse.getInput());
            ToolCallSignature existing = seenApprovalCalls.putIfAbsent(normalizedToolUse.getId(), signature);
            if (existing != null) {
                if (!existing.equals(signature)) {
                    throw new AgentRuntimeException(
                            "Duplicate toolCallId is bound to a different tool call: " + normalizedToolUse.getId());
                }
                // 相同 toolCallId 表示同一协议调用被重复返回，只保留第一次出现。
                changed = true;
                continue;
            }
            normalizedContent.add(normalizedToolUse);
        }
        if (!changed) {
            return reasoningMessage;
        }
        return Msg.builder()
                .id(reasoningMessage.getId())
                .name(reasoningMessage.getName())
                .role(reasoningMessage.getRole())
                .content(normalizedContent)
                .metadata(reasoningMessage.getMetadata())
                .timestamp(reasoningMessage.getTimestamp())
                .build();
    }

    /**
     * 为缺少调用ID的工具生成跨同一推理消息重放稳定的调用ID。
     *
     * @param reasoningMessage 推理消息
     * @param contentIndex 工具块在消息内容中的位置
     * @return 稳定工具调用ID
     */
    private String stableToolCallId(Msg reasoningMessage, int contentIndex) {
        String messageId = reasoningMessage.getId();
        if (messageId == null || messageId.isBlank()) {
            return "hitl-" + UUID.randomUUID();
        }
        return "hitl-" + messageId + "-" + contentIndex;
    }

    /**
     * 为同一推理消息生成稳定审批批次ID。
     *
     * @param reasoningMessage 推理消息
     * @return 审批批次ID
     */
    private String approvalBatchId(Msg reasoningMessage) {
        String messageId = reasoningMessage == null ? null : reasoningMessage.getId();
        if (messageId == null || messageId.isBlank()) {
            return "hitl-batch-" + UUID.randomUUID();
        }
        return "hitl-batch-" + messageId;
    }

    /**
     * 判断工具调用是否要求人工审批。
     *
     * @param toolUse 工具调用
     * @return 要求审批时为 true
     */
    private boolean isApprovalRequired(ToolUseBlock toolUse) {
        AgentToolSpec toolSpec = toolUse == null ? null : toolSpecs.get(toolUse.getName());
        return requiresApproval(toolSpec, toolUse);
    }

    /**
     * 判断工具声明或当前调用是否要求审批。
     *
     * @param toolSpec 工具声明
     * @param toolUse 当前工具调用
     * @return 需要审批时为 true
     */
    private boolean requiresApproval(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
        if (toolSpec == null) {
            return false;
        }
        AgentToolApprovalEvaluation evaluation = approvalEvaluation(toolSpec, toolUse);
        if (evaluation != null && !evaluation.valid()) {
            // 无效命令直接进入工具并返回结构化拒绝，避免产生必然失败的审批请求。
            return false;
        }
        return toolSpec.isApprovalRequired()
                || (evaluation != null && evaluation.approvalRequired())
                || requiresLegacyForcedApproval(toolSpec, toolUse);
    }

    /**
     * 根据工具声明中的强制审批命令规则检查当前调用。
     *
     * <p>命令首词解析与受控 Shell 的引号、反斜杠规则保持一致，避免通过
     * {@code 'rm'} 或 {@code r\m} 绕过动态审批。畸形命令仍由 Shell 工具拒绝。</p>
     *
     * @param toolSpec 工具声明
     * @param toolUse 当前工具调用
     * @return 命中强制审批命令时为 true
     */
    private boolean requiresForcedApproval(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
        AgentToolApprovalEvaluation evaluation = approvalEvaluation(toolSpec, toolUse);
        if (evaluation != null) {
            return evaluation.valid() && evaluation.forced();
        }
        return requiresLegacyForcedApproval(toolSpec, toolUse);
    }

    /**
     * 使用旧版元数据规则判断当前调用是否命中强制审批命令。
     *
     * @param toolSpec 工具声明
     * @param toolUse 当前工具调用
     * @return 命中旧版强制审批规则时为 true
     */
    private boolean requiresLegacyForcedApproval(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
        if (toolSpec == null || toolUse == null || toolSpec.getMetadata() == null) {
            return false;
        }
        Object commandsValue = toolSpec.getMetadata().get("forceApprovalCommands");
        Object argumentValue = toolSpec.getMetadata().get("forceApprovalCommandArgument");
        if (!(commandsValue instanceof Iterable<?> commands) || !(argumentValue instanceof String argumentName)
                || argumentName.isBlank() || toolUse.getInput() == null) {
            return false;
        }
        Object commandValue = toolUse.getInput().get(argumentName);
        if (!(commandValue instanceof String command)) {
            return false;
        }
        String executable = firstCommandToken(command);
        if (executable == null) {
            return false;
        }
        for (Object forcedCommand : commands) {
            if (forcedCommand instanceof String value && executable.equals(value)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 解析受限命令行的首个参数。
     *
     * @param command 命令行
     * @return 首个参数；无有效参数时返回 null
     */
    private String firstCommandToken(String command) {
        if (command == null || command.isBlank()) {
            return null;
        }
        StringBuilder token = new StringBuilder();
        char quote = 0;
        boolean escaping = false;
        boolean started = false;
        for (int index = 0; index < command.length(); index++) {
            char character = command.charAt(index);
            if (!started && Character.isWhitespace(character)) {
                continue;
            }
            started = true;
            if (escaping) {
                token.append(character);
                escaping = false;
            } else if (character == '\\' && quote != '\'') {
                escaping = true;
            } else if (character == '\'' || character == '"') {
                if (quote == 0) {
                    quote = character;
                } else if (quote == character) {
                    quote = 0;
                } else {
                    token.append(character);
                }
            } else if (Character.isWhitespace(character) && quote == 0) {
                break;
            } else {
                token.append(character);
            }
        }
        return token.isEmpty() ? null : token.toString();
    }

    /**
     * 复制工具调用并替换调用ID。
     *
     * @param toolUse 原始工具调用
     * @param toolCallId 新工具调用ID
     * @return 新工具调用
     */
    private ToolUseBlock copyWithId(ToolUseBlock toolUse, String toolCallId) {
        return ToolUseBlock.builder()
                .id(toolCallId)
                .name(toolUse.getName())
                .input(toolUse.getInput())
                .content(toolUse.getContent())
                .metadata(toolUse.getMetadata())
                .build();
    }

    /**
     * 获取推理消息中要求审批的工具调用。
     *
     * @param reasoningMessage 推理消息
     * @return 待审批工具调用
     */
    private List<ToolUseBlock> approvalRequiredTools(Msg reasoningMessage) {
        List<ToolUseBlock> toolUses = reasoningMessage.getContentBlocks(ToolUseBlock.class);
        if (toolUses == null || toolUses.isEmpty()) {
            return List.of();
        }
        List<ToolUseBlock> approvalTools = new ArrayList<>();
        Set<String> pendingReusableScopes = new LinkedHashSet<>();
        for (ToolUseBlock toolUse : toolUses) {
            AgentToolSpec toolSpec = toolUse == null ? null : toolSpecs.get(toolUse.getName());
            if (!requiresApproval(toolSpec, toolUse)) {
                continue;
            }
            Map<String, Object> approvalMetadata = approvalMetadata(toolSpec, toolUse);
            if (!requiresForcedApproval(toolSpec, toolUse)
                    && approvalCoordinator.isReusableApprovalGranted(approvalMetadata)) {
                continue;
            }
            String reusableScope = approvalCoordinator.reusableApprovalScope(approvalMetadata);
            if (reusableScope != null && !pendingReusableScopes.add(reusableScope)) {
                // 同一推理消息中同一 MCP 的多个工具共享一个审批请求。
                continue;
            }
            approvalTools.add(toolUse);
        }
        return approvalTools;
    }

    /**
     * 注册审批批次中的待审批状态。
     *
     * @param toolSpec 工具声明
     * @param toolUse 工具调用
     * @param approvalBatchId 审批批次ID
     * @return 待审批状态
     */
    private AgentPendingState registerPendingState(AgentToolSpec toolSpec,
                                                   ToolUseBlock toolUse,
                                                   String approvalBatchId) {
        AgentRuntimeExecutionContext context = eventBridge.executionContext();
        AgentToolApprovalRequest approvalRequest = toolSpec.getApprovalRequest();
        Duration timeout = approvalRequest == null || approvalRequest.getTimeout() == null
                ? Duration.ofMinutes(30)
                : approvalRequest.getTimeout();
        Map<String, Object> metadata = approvalRequest == null
                ? new LinkedHashMap<>()
                : new LinkedHashMap<>(approvalRequest.getMetadata());
        metadata.putAll(toolUse.getMetadata() == null ? Map.of() : toolUse.getMetadata());
        if (toolSpec.getMetadata() != null && !toolSpec.getMetadata().isEmpty()) {
            // ToolSpec 由工具编译阶段生成，必须覆盖模型返回的同名治理字段（例如 toolType、mcpId）。
            metadata.putAll(toolSpec.getMetadata());
        }
        AgentToolApprovalEvaluation evaluation = approvalEvaluation(toolSpec, toolUse);
        if (evaluation != null && evaluation.metadata() != null) {
            // 动态策略由受信任工具实例计算，必须覆盖模型与静态声明中的同名字段。
            metadata.putAll(evaluation.metadata());
        }
        metadata.put("phase", "POST_REASONING");
        metadata.put("source", "TOOL_HITL_INTERCEPTOR");
        return approvalCoordinator.register(
                context == null ? null : context.getSessionId(),
                context == null || context.getAgentDefinition() == null ? null : context.getAgentDefinition().getAgentId(),
                toolUse.getId(),
                toolUse.getName(),
                approvalPrompt(approvalRequest),
                toolUse.getInput(),
                metadata,
                Instant.now().plus(timeout),
                approvalBatchId);
    }

    /**
     * 调用工具声明中的受信任动态审批策略。
     *
     * @param toolSpec 工具声明
     * @param toolUse 当前工具调用
     * @return 动态审批判定；未配置策略时返回 null
     */
    private AgentToolApprovalEvaluation approvalEvaluation(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
        if (toolSpec == null || toolSpec.getApprovalPolicy() == null || toolUse == null) {
            return null;
        }
        return toolSpec.getApprovalPolicy().evaluate(
                toolUse.getInput() == null ? Map.of() : toolUse.getInput());
    }

    /**
     * 合并静态工具元数据与动态审批元数据。
     *
     * @param toolSpec 工具声明
     * @param toolUse 当前工具调用
     * @return 用于审批作用域判断的受信任元数据
     */
    private Map<String, Object> approvalMetadata(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
        Map<String, Object> metadata = new LinkedHashMap<>();
        if (toolSpec != null && toolSpec.getMetadata() != null) {
            metadata.putAll(toolSpec.getMetadata());
        }
        AgentToolApprovalEvaluation evaluation = approvalEvaluation(toolSpec, toolUse);
        if (evaluation != null && evaluation.metadata() != null) {
            metadata.putAll(evaluation.metadata());
        }
        return metadata;
    }

    /**
     * 构建工具审批请求事件。
     *
     * @param toolSpec 工具声明
     * @param toolUse 工具调用
     * @param pendingState 待审批状态
     * @return 审批请求事件
     */
    private AgentRuntimeEvent toolApprovalRequiredEvent(AgentToolSpec toolSpec,
                                                       ToolUseBlock toolUse,
                                                       AgentPendingState pendingState) {
        AgentRuntimeExecutionContext context = eventBridge.executionContext();
        AgentRuntimeEvent event = eventBridge.event(AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED);
        event.setToolCallId(toolUse.getId());
        event.getPayload().putAll(pendingApprovalPayload(pendingState, toolUse));
        event.getPayload().put("sessionId", context == null ? null : context.getSessionId());
        event.getPayload().put("agentId", context == null || context.getAgentDefinition() == null
                ? null
                : context.getAgentDefinition().getAgentId());
        event.getPayload().put("approvalPrompt", approvalPrompt(toolSpec.getApprovalRequest()));
        event.getPayload().put("approvalMetadata", pendingState.getMetadata());
        event.getPayload().put("toolDescription", toolSpec.getDescription());
        enrichToolPayload(event.getPayload(), toolSpec);
        event.getMetadata().put("source", "TOOL_HITL_INTERCEPTOR");
        event.getMetadata().put("phase", "POST_REASONING");
        return event;
    }

    /**
     * 构建待审批工具的稳定事件载荷。
     *
     * @param pendingState 待审批状态
     * @param toolUse 工具调用
     * @return 待审批载荷
     */
    private Map<String, Object> pendingApprovalPayload(AgentPendingState pendingState, ToolUseBlock toolUse) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("resumeToken", pendingState.getResumeToken().getValue());
        payload.put("toolCallId", pendingState.getToolCallId());
        payload.put("toolName", pendingState.getToolName());
        payload.put("toolInput", pendingState.getToolInput());
        payload.put("input", toolUse.getInput());
        payload.put("content", toolUse.getContent());
        payload.put("expiresAt", pendingState.getExpiresAt() == null ? null : pendingState.getExpiresAt().toString());
        return payload;
    }

    /**
     * 将工具展示元数据补充到审批事件载荷。
     *
     * @param payload 审批事件载荷
     * @param toolSpec 工具声明
     */
    private void enrichToolPayload(Map<String, Object> payload, AgentToolSpec toolSpec) {
        if (toolSpec == null || toolSpec.getMetadata() == null || toolSpec.getMetadata().isEmpty()) {
            return;
        }
        Map<String, Object> metadata = toolSpec.getMetadata();
        putIfPresent(payload, metadata, "toolDisplayName");
        putIfPresent(payload, metadata, "rawMcpToolName");
        putIfPresent(payload, metadata, "mcpToolName");
        putIfPresent(payload, metadata, "mcpId");
        putIfPresent(payload, metadata, "mcpName");
        putIfPresent(payload, metadata, "mcpTitle");
    }

    /**
     * 在元数据包含指定字段时复制到事件载荷。
     *
     * @param payload 事件载荷
     * @param metadata 工具元数据
     * @param key 字段名
     */
    private void putIfPresent(Map<String, Object> payload, Map<String, Object> metadata, String key) {
        if (metadata.containsKey(key)) {
            payload.put(key, metadata.get(key));
        }
    }

    /**
     * 获取审批提示文案。
     *
     * @param approvalRequest 审批配置
     * @return 审批提示文案
     */
    private String approvalPrompt(AgentToolApprovalRequest approvalRequest) {
        if (approvalRequest != null
                && approvalRequest.getApprovalPrompt() != null
                && !approvalRequest.getApprovalPrompt().isBlank()) {
            return approvalRequest.getApprovalPrompt();
        }
        return "是否批准执行该工具？";
    }

    /**
     * 工具名称与输入组成的调用身份校验值。
     */
    private static final class ToolCallSignature {
        private final String toolName;
        private final Map<String, Object> toolInput;

        /**
         * 创建工具调用身份校验值。
         *
         * @param toolName 工具名称
         * @param toolInput 工具入参
         */
        private ToolCallSignature(String toolName, Map<String, Object> toolInput) {
            this.toolName = toolName;
            this.toolInput = toolInput == null ? Map.of() : new LinkedHashMap<>(toolInput);
        }

        /**
         * 比较工具调用语义是否一致。
         *
         * @param object 待比较对象
         * @return 语义一致时为 true
         */
        @Override
        public boolean equals(Object object) {
            if (this == object) {
                return true;
            }
            if (!(object instanceof ToolCallSignature that)) {
                return false;
            }
            return Objects.equals(toolName, that.toolName)
                    && Objects.equals(toolInput, that.toolInput);
        }

        /**
         * 计算工具调用语义哈希。
         *
         * @return 哈希值
         */
        @Override
        public int hashCode() {
            return Objects.hash(toolName, toolInput);
        }
    }
}
