package com.easyagents.agent.runtime.mcp;

import com.alibaba.fastjson2.JSON;
import com.easyagents.agent.runtime.AgentRuntimeException;
import io.modelcontextprotocol.spec.McpSchema;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * MCP Tool 冻结清单规范化与完整性校验器。
 */
public final class McpToolManifest {

    /** MCP Tool 原始名称允许的最大 Unicode 字符数。 */
    public static final int MAX_TOOL_NAME_LENGTH = 128;

    /** MCP Tool 描述允许的最大 Unicode 字符数。 */
    public static final int MAX_TOOL_DESCRIPTION_LENGTH = 4_096;

    /** 单个输入或输出 Schema 允许的最大 UTF-8 字节数。 */
    public static final int MAX_SCHEMA_UTF8_BYTES = 256 * 1_024;

    /** 完整规范化 Manifest 允许的最大 UTF-8 字节数。 */
    public static final int MAX_MANIFEST_UTF8_BYTES = 2 * 1_024 * 1_024;

    private McpToolManifest() {
    }

    /**
     * 将 MCP Tool 转换为稳定清单项。
     *
     * @param tools MCP Tool 列表
     * @return 按名称稳定排序的清单
     */
    public static List<McpToolManifestEntry> fromTools(List<McpSchema.Tool> tools) {
        if (tools == null || tools.isEmpty()) {
            return List.of();
        }
        List<McpToolManifestEntry> entries = new ArrayList<>();
        Set<String> names = new HashSet<>();
        int manifestBytes = 2;
        for (McpSchema.Tool tool : tools) {
            if (tool == null || tool.name() == null || tool.name().isBlank()) {
                continue;
            }
            if (!names.add(tool.name())) {
                throw new AgentRuntimeException("Duplicate MCP tool name: " + tool.name());
            }
            McpToolManifestEntry entry = new McpToolManifestEntry();
            entry.setName(tool.name());
            entry.setDescription(normalizeText(tool.description()));
            entry.setInputSchema(normalizeSchema("MCP tool input schema", tool.inputSchema()));
            entry.setOutputSchema(normalizeSchema("MCP tool output schema", tool.outputSchema()));
            assertEntryBounds(entry);
            manifestBytes += JSON.toJSONString(toCanonicalValue(entry))
                    .getBytes(StandardCharsets.UTF_8).length;
            if (!entries.isEmpty()) {
                manifestBytes++;
            }
            if (manifestBytes > MAX_MANIFEST_UTF8_BYTES) {
                throw new AgentRuntimeException("MCP tool manifest exceeds "
                        + MAX_MANIFEST_UTF8_BYTES + " UTF-8 bytes.");
            }
            entries.add(entry);
        }
        entries.sort(Comparator.comparing(McpToolManifestEntry::getName));
        assertManifestSize(entries);
        return List.copyOf(entries);
    }

    /**
     * 计算冻结清单的 SHA-256。
     *
     * @param entries 冻结清单
     * @return 十六进制 SHA-256
     */
    public static String hash(List<McpToolManifestEntry> entries) {
        String json = canonicalJson(entries);
        try {
            byte[] digest = MessageDigest.getInstance("SHA-256")
                    .digest(json.getBytes(StandardCharsets.UTF_8));
            return java.util.HexFormat.of().formatHex(digest);
        } catch (NoSuchAlgorithmException error) {
            throw new AgentRuntimeException("SHA-256 is unavailable for MCP manifest validation.", error);
        }
    }

    /**
     * 校验远端 Tool 与冻结白名单一致，同时忽略远端新增 Tool。
     *
     * @param spec MCP 运行时声明
     * @param actualTools 远端当前 Tool
     * @throws AgentRuntimeException 冻结清单缺失、被篡改、Tool 缺失或 Schema 漂移时抛出
     */
    public static void assertFrozenManifest(McpSpec spec, List<McpSchema.Tool> actualTools) {
        if (spec == null || spec.getSkillId() == null || spec.getSkillId().isBlank()) {
            return;
        }
        List<McpToolManifestEntry> expected = spec.getFrozenToolManifest();
        String expectedHash = spec.getFrozenToolManifestHash();
        if (expected == null || expected.isEmpty() || expectedHash == null || expectedHash.isBlank()) {
            throw new AgentRuntimeException("Skill-bound MCP requires a frozen tool manifest: " + spec.getName());
        }
        if (!expectedHash.equals(hash(expected))) {
            throw new AgentRuntimeException("Skill-bound MCP frozen tool manifest is invalid: " + spec.getName());
        }
        Set<String> frozenNames = new HashSet<>();
        for (McpToolManifestEntry entry : expected) {
            if (entry != null && entry.getName() != null && !entry.getName().isBlank()) {
                frozenNames.add(entry.getName());
            }
        }
        Map<String, McpToolManifestEntry> actualByName = new LinkedHashMap<>();
        if (actualTools != null) {
            for (McpSchema.Tool tool : actualTools) {
                if (tool == null || tool.name() == null || !frozenNames.contains(tool.name())) {
                    continue;
                }
                if (actualByName.containsKey(tool.name())) {
                    throw new AgentRuntimeException("Duplicate MCP tool name: " + tool.name());
                }
                List<McpToolManifestEntry> normalized = fromTools(List.of(tool));
                if (!normalized.isEmpty()) {
                    actualByName.put(tool.name(), normalized.get(0));
                }
            }
        }
        for (McpToolManifestEntry expectedEntry : expected) {
            if (expectedEntry == null || expectedEntry.getName() == null || expectedEntry.getName().isBlank()) {
                throw new AgentRuntimeException("Skill-bound MCP frozen tool name is required: " + spec.getName());
            }
            McpToolManifestEntry actualEntry = actualByName.get(expectedEntry.getName());
            if (actualEntry == null) {
                throw new AgentRuntimeException("Skill-bound MCP tool is missing: " + expectedEntry.getName());
            }
            if (!sameRuntimeSchema(expectedEntry, actualEntry)) {
                throw new AgentRuntimeException("Skill-bound MCP tool schema has changed: "
                        + expectedEntry.getName());
            }
        }
    }

    /**
     * 比较 Runtime 必须锁定的 Tool 名称及输入、输出 Schema。
     *
     * <p>描述用于保存与发布阶段的完整 manifest 变更识别，但远端仅调整描述时不会改变
     * 已发布 Tool 的可调用边界，因此运行时不应中断既有 Agent。</p>
     *
     * @param expected 冻结清单项
     * @param actual 远端当前清单项
     * @return 名称及 Schema 相同时返回 {@code true}
     */
    private static boolean sameRuntimeSchema(McpToolManifestEntry expected,
                                             McpToolManifestEntry actual) {
        return java.util.Objects.equals(expected.getName(), actual.getName())
                && java.util.Objects.equals(normalizeJson(expected.getInputSchema()),
                normalizeJson(actual.getInputSchema()))
                && java.util.Objects.equals(normalizeJson(expected.getOutputSchema()),
                normalizeJson(actual.getOutputSchema()));
    }

    /**
     * 将冻结清单转换为稳定 JSON，并对反序列化后的清单执行同等边界校验。
     *
     * @param entries 冻结清单
     * @return 稳定 JSON
     * @throws AgentRuntimeException 清单包含重复名称或超出预算时抛出
     */
    private static String canonicalJson(List<McpToolManifestEntry> entries) {
        List<Map<String, Object>> canonical = new ArrayList<>();
        Set<String> names = new HashSet<>();
        if (entries != null) {
            entries.stream()
                    .filter(entry -> entry != null && entry.getName() != null && !entry.getName().isBlank())
                    .sorted(Comparator.comparing(McpToolManifestEntry::getName))
                    .forEach(entry -> {
                        if (!names.add(entry.getName())) {
                            throw new AgentRuntimeException("Duplicate MCP tool name: " + entry.getName());
                        }
                        McpToolManifestEntry normalized = new McpToolManifestEntry();
                        normalized.setName(entry.getName());
                        normalized.setDescription(normalizeText(entry.getDescription()));
                        normalized.setInputSchema(normalizeSchema(
                                "MCP tool input schema", entry.getInputSchema()));
                        normalized.setOutputSchema(normalizeSchema(
                                "MCP tool output schema", entry.getOutputSchema()));
                        assertEntryBounds(normalized);
                        canonical.add(toCanonicalValue(normalized));
                    });
        }
        String json = JSON.toJSONString(canonical);
        assertUtf8Size("MCP tool manifest", json, MAX_MANIFEST_UTF8_BYTES);
        return json;
    }

    /**
     * 校验单个清单项的名称、描述及 Schema 预算。
     *
     * @param entry 已规范化的清单项
     * @throws AgentRuntimeException 任一字段超出预算时抛出
     */
    private static void assertEntryBounds(McpToolManifestEntry entry) {
        assertTextLength("MCP tool name", entry.getName(), MAX_TOOL_NAME_LENGTH);
        assertTextLength("MCP tool description", entry.getDescription(), MAX_TOOL_DESCRIPTION_LENGTH);
        assertSchemaSize("MCP tool input schema", entry.getInputSchema());
        assertSchemaSize("MCP tool output schema", entry.getOutputSchema());
    }

    /**
     * 校验规范化清单的聚合字节预算。
     *
     * @param entries 已规范化且排序的清单
     * @throws AgentRuntimeException 清单超出聚合预算时抛出
     */
    private static void assertManifestSize(List<McpToolManifestEntry> entries) {
        List<Map<String, Object>> canonical = entries.stream()
                .map(McpToolManifest::toCanonicalValue)
                .toList();
        assertUtf8Size("MCP tool manifest", JSON.toJSONString(canonical), MAX_MANIFEST_UTF8_BYTES);
    }

    /**
     * 构造用于哈希和预算计算的稳定清单值。
     *
     * @param entry 已规范化的清单项
     * @return 保持字段顺序的清单值
     */
    private static Map<String, Object> toCanonicalValue(McpToolManifestEntry entry) {
        Map<String, Object> value = new LinkedHashMap<>();
        value.put("name", entry.getName());
        value.put("description", normalizeText(entry.getDescription()));
        value.put("inputSchema", entry.getInputSchema());
        value.put("outputSchema", entry.getOutputSchema());
        return value;
    }

    /**
     * 校验 Unicode 字符长度，避免 UTF-16 代理对被重复计数。
     *
     * @param field 字段名称
     * @param value 字段值
     * @param maxLength 最大 Unicode 字符数
     * @throws AgentRuntimeException 字段超长时抛出
     */
    private static void assertTextLength(String field, String value, int maxLength) {
        if (value != null && value.codePointCount(0, value.length()) > maxLength) {
            throw new AgentRuntimeException(field + " exceeds " + maxLength + " characters.");
        }
    }

    /**
     * 校验单个 Schema 的 UTF-8 字节预算。
     *
     * @param field Schema 字段名称
     * @param schema 已规范化 Schema
     * @throws AgentRuntimeException Schema 超出预算时抛出
     */
    private static void assertSchemaSize(String field, Object schema) {
        if (schema != null) {
            assertUtf8Size(field, JSON.toJSONString(schema), MAX_SCHEMA_UTF8_BYTES);
        }
    }

    /**
     * 校验 JSON 或文本的 UTF-8 字节长度。
     *
     * @param field 字段名称
     * @param value 待校验文本
     * @param maxBytes 最大 UTF-8 字节数
     * @throws AgentRuntimeException 文本超出预算时抛出
     */
    private static void assertUtf8Size(String field, String value, int maxBytes) {
        int bytes = value.getBytes(StandardCharsets.UTF_8).length;
        if (bytes > maxBytes) {
            throw new AgentRuntimeException(field + " exceeds " + maxBytes + " UTF-8 bytes.");
        }
    }

    /**
     * 在解析和排序前限制原始 Schema，避免超大输入进入规范化流程。
     *
     * @param field Schema 字段名称
     * @param value 原始 Schema
     * @return 规范化 Schema
     * @throws AgentRuntimeException 原始 Schema 超出预算时抛出
     */
    private static Object normalizeSchema(String field, Object value) {
        if (value == null) {
            return null;
        }
        String json = JSON.toJSONString(value);
        assertUtf8Size(field, json, MAX_SCHEMA_UTF8_BYTES);
        return sortJson(JSON.parse(json));
    }

    private static Object normalizeJson(Object value) {
        if (value == null) {
            return null;
        }
        return sortJson(JSON.parse(JSON.toJSONString(value)));
    }

    private static Object sortJson(Object value) {
        if (value instanceof Map<?, ?> source) {
            Map<String, Object> sorted = new TreeMap<>();
            source.forEach((key, child) -> sorted.put(String.valueOf(key), sortJson(child)));
            return sorted;
        }
        if (value instanceof List<?> source) {
            List<Object> sorted = new ArrayList<>(source.size());
            for (Object child : source) {
                sorted.add(sortJson(child));
            }
            return sorted;
        }
        return value;
    }

    private static String normalizeText(String value) {
        return value == null ? "" : value;
    }
}
