package com.gzzm.lobster.tool.mcp;

import com.gzzm.lobster.common.JsonUtil;
import com.gzzm.lobster.common.IdGenerator;
import com.gzzm.lobster.common.LobsterException;
import com.gzzm.lobster.common.ToolCategory;
import com.gzzm.lobster.common.ToolExecutionMode;
import com.gzzm.lobster.common.ToolRiskLevel;
import com.gzzm.lobster.config.LobsterConfig;
import com.gzzm.lobster.tool.BuiltinToolDefinition;
import com.gzzm.lobster.tool.SideEffectLevel;
import com.gzzm.lobster.tool.ToolContext;
import com.gzzm.lobster.tool.ToolDefinitionConfig;
import com.gzzm.lobster.tool.ToolDefinitionConfigDao;
import com.gzzm.lobster.tool.ToolExecutor;
import com.gzzm.lobster.tool.ToolRegistry;
import com.gzzm.lobster.tool.ToolResult;
import com.gzzm.lobster.storage.FileSystemContentStore;
import com.gzzm.platform.commons.Tools;
import net.cyan.nest.annotation.Inject;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * MCP 工具桥接。
 *
 * <p>从已启用 MCP Server 真实 discovery 远端工具，写入发现缓存，然后注册到
 * Lobster 的 {@link ToolRegistry}。模型调用时仍然统一经过
 * {@link com.gzzm.lobster.tool.ToolExecutorDispatcher}，因此权限、限流、审计、
 * 执行记录和重放保护都保留在 Lobster 侧。
 */
public class McpToolBridge {

    private static final long DEFAULT_REFRESH_MS = 600_000L;
    private static final Object AUTO_REFRESH_LOCK = new Object();
    private static volatile ScheduledExecutorService autoRefreshExecutor;

    @Inject private McpServerConfigDao serverConfigDao;
    @Inject private McpToolCacheDao toolCacheDao;
    @Inject private McpCallLogDao mcpCallLogDao;
    @Inject private ToolDefinitionConfigDao toolDefinitionConfigDao;
    @Inject private ToolRegistry toolRegistry;
    @Inject private FileSystemContentStore contentStore;
    @Inject private McpRuntimeToolCache runtimeToolCache;

    private final McpClientFactory clientFactory = new McpClientFactory();

    public void startAutoRefresh() {
        long intervalMs = mcpToolListTtlMs();
        if (!LobsterConfig.isMcpToolAutoRefreshEnabled() || intervalMs <= 0L) {
            try { Tools.log("[McpToolBridge] MCP tool auto refresh disabled"); }
            catch (Throwable ignore) { /* ignore */ }
            return;
        }
        synchronized (AUTO_REFRESH_LOCK) {
            if (autoRefreshExecutor != null && !autoRefreshExecutor.isShutdown()) {
                return;
            }
            autoRefreshExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
                Thread t = new Thread(r, "lobster-mcp-tool-refresh");
                t.setDaemon(true);
                return t;
            });
            autoRefreshExecutor.scheduleWithFixedDelay(new Runnable() {
                @Override
                public void run() {
                    scheduledRefresh();
                }
            }, intervalMs, intervalMs, TimeUnit.MILLISECONDS);
        }
        try { Tools.log("[McpToolBridge] MCP tool auto refresh started, intervalMs=" + intervalMs); }
        catch (Throwable ignore) { /* ignore */ }
    }

    public void stopAutoRefresh() {
        synchronized (AUTO_REFRESH_LOCK) {
            if (autoRefreshExecutor != null) {
                autoRefreshExecutor.shutdownNow();
                autoRefreshExecutor = null;
            }
        }
    }

    private void scheduledRefresh() {
        try {
            int registered = refresh();
            try { Tools.log("[McpToolBridge] scheduled MCP listTools refresh done, registered=" + registered); }
            catch (Throwable ignore) { /* ignore */ }
        } catch (Throwable t) {
            try { Tools.log("[McpToolBridge] scheduled MCP listTools refresh failed", t); }
            catch (Throwable ignore) { /* ignore */ }
        }
    }

    public int refresh() {
        try {
            List<McpServerConfig> servers = serverConfigDao().listEnabled();
            int registered = 0;
            for (McpServerConfig srv : servers) {
                if (srv == null || srv.getServerId() == null) continue;
                try {
                    registered += discoverAndRegister(srv, false).registered;
                } catch (Exception e) {
                    try { Tools.log("[McpToolBridge] refresh server failed: " + srv.getServerId(), e); }
                    catch (Throwable ignore) { /* ignore */ }
                }
            }
            return registered;
        } catch (Exception e) {
            try { Tools.log("[McpToolBridge] refresh failed", e); } catch (Throwable ignore) { /* ignore */ }
            return 0;
        }
    }

    public DiscoverSummary discoverServer(String serverId, boolean force) {
        try {
            McpServerConfig server = serverConfigDao().getConfig(serverId);
            if (server == null) {
                throw new LobsterException("mcp.server_not_found", "MCP server not found: " + serverId);
            }
            if (Boolean.FALSE.equals(server.getEnabled())) {
                unregisterServer(serverId);
                DiscoverSummary summary = new DiscoverSummary();
                summary.serverId = serverId;
                return summary;
            }
            return discoverAndRegister(server, force);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new LobsterException("mcp.discover_failed", "MCP discovery failed: " + e.getMessage(), e);
        }
    }

    public Map<String, Object> testServer(String serverId) {
        long start = System.currentTimeMillis();
        try {
            McpServerConfig server = serverConfigDao().getConfig(serverId);
            if (server == null) {
                throw new LobsterException("mcp.server_not_found", "MCP server not found: " + serverId);
            }
            List<McpToolSpec> tools = clientFactory.create(server).listTools(server, defaultOptions("test"));
            Map<String, Object> out = new LinkedHashMap<>();
            out.put("serverId", serverId);
            out.put("ok", Boolean.TRUE);
            out.put("transportType", server.getTransportType());
            out.put("latencyMs", System.currentTimeMillis() - start);
            out.put("toolCount", tools.size());
            out.put("message", "connected");
            return out;
        } catch (Exception e) {
            Map<String, Object> out = new LinkedHashMap<>();
            out.put("serverId", serverId);
            out.put("ok", Boolean.FALSE);
            out.put("latencyMs", System.currentTimeMillis() - start);
            out.put("message", e.getMessage());
            return out;
        }
    }

    public void unregisterServer(String serverId) {
        runtimeToolCache().invalidateServer(serverId);
        try {
            List<McpToolCache> rows = toolCacheDao().listByServer(serverId);
            for (McpToolCache row : rows) {
                if (row != null && row.getLocalToolName() != null) {
                    toolRegistry.unregister(row.getLocalToolName());
                }
            }
        } catch (Throwable t) {
            try { Tools.log("[McpToolBridge] unregister server failed: " + serverId, t); }
            catch (Throwable ignore) { /* ignore */ }
        }
    }

    public void registerCachedTool(String localToolName) {
        try {
            McpRuntimeToolCache.Snapshot snapshot = runtimeToolCache().refreshLocal(localToolName);
            if (snapshot == null) {
                toolRegistry.unregister(localToolName);
                return;
            }
            registerCacheTool(snapshot.getServer(), snapshot.getTool());
        } catch (Throwable t) {
            try { Tools.log("[McpToolBridge] register cached tool failed: " + localToolName, t); }
            catch (Throwable ignore) { /* ignore */ }
        }
    }

    public void refreshRuntimeServer(String serverId) {
        try {
            List<McpRuntimeToolCache.Snapshot> snapshots = runtimeToolCache().refreshServer(serverId);
            if (snapshots == null) {
                return;
            }
            if (snapshots.isEmpty()) {
                unregisterServer(serverId);
                return;
            }
            for (McpRuntimeToolCache.Snapshot snapshot : snapshots) {
                registerCacheTool(snapshot.getServer(), snapshot.getTool());
            }
        } catch (Throwable t) {
            try { Tools.log("[McpToolBridge] refresh runtime server failed: " + serverId, t); }
            catch (Throwable ignore) { /* ignore */ }
        }
    }

    private DiscoverSummary discoverAndRegister(McpServerConfig server, boolean force) throws Exception {
        List<McpToolSpec> specs;
        try {
            specs = clientFactory.create(server).listTools(server, defaultOptions("discover"));
        } catch (Exception e) {
            markServerDiscoveryError(server, e);
            throw e;
        }
        int registered = 0;
        int disabled = 0;
        Set<String> seen = new HashSet<>();
        Date now = new Date();
        for (McpToolSpec spec : specs) {
            if (spec == null || spec.getName() == null || spec.getName().trim().isEmpty()) continue;
            McpToolCache cache = upsertCache(server, spec, now);
            seen.add(cache.getToolKey());
            if (!isDirectlyExposed(cache)) {
                runtimeToolCache().invalidateLocal(cache.getLocalToolName());
                toolRegistry.unregister(cache.getLocalToolName());
                disabled++;
                continue;
            }
            registerCacheTool(server, cache);
            registered++;
        }
        for (McpToolCache old : toolCacheDao().listByServer(server.getServerId())) {
            if (old == null || old.getToolKey() == null || seen.contains(old.getToolKey())) continue;
            if (old.getLocalToolName() != null) toolRegistry.unregister(old.getLocalToolName());
            if (old.getLocalToolName() != null) runtimeToolCache().invalidateLocal(old.getLocalToolName());
            old.setEnabled(Boolean.FALSE);
            old.setLastErrorCode("not_discovered");
            old.setLastErrorMessage("Tool was not returned by the latest discovery");
            old.setUpdateTime(now);
            toolCacheDao().save(old);
            disabled++;
        }
        DiscoverSummary summary = new DiscoverSummary();
        summary.serverId = server.getServerId();
        summary.discovered = specs.size();
        summary.registered = registered;
        summary.disabled = disabled;
        return summary;
    }

    private McpToolCache upsertCache(McpServerConfig server, McpToolSpec spec, Date now) throws Exception {
        String localName = localToolName(server, spec.getName());
        String toolKey = server.getServerId() + ":" + server.getNamespace() + ":" + spec.getName();
        McpToolCache row = toolCacheDao().getByKey(toolKey);
        boolean isNew = row == null;
        String oldLocalName = isNew ? null : row.getLocalToolName();
        if (isNew) {
            row = new McpToolCache();
            row.setToolKey(toolKey);
            row.setRiskLevel(spec.getRiskLevel() == null ? defaultRisk(server) : spec.getRiskLevel());
            row.setRequireConfirm(Boolean.valueOf(spec.isRequireConfirm()));
            row.setEnabled(Boolean.TRUE);
            row.setExposureMode(McpToolExposureMode.DIRECT);
            row.setCreateTime(now);
        }
        row.setServerId(server.getServerId());
        row.setNamespace(server.getNamespace());
        row.setOrgId(server.getOrgId());
        row.setRemoteToolName(spec.getName());
        row.setLocalToolName(localName);
        row.setDisplayName(firstNonBlank(spec.getTitle(), localName));
        row.setDescription(spec.getDescription());
        row.setInputSchemaJson(JsonUtil.toJson(spec.getInputSchema()));
        row.setOutputSchemaJson(spec.getOutputSchema() == null || spec.getOutputSchema().isEmpty()
                ? null : JsonUtil.toJson(spec.getOutputSchema()));
        row.setAnnotationsJson(spec.getAnnotations() == null || spec.getAnnotations().isEmpty()
                ? null : JsonUtil.toJson(spec.getAnnotations()));
        row.setSchemaHash(schemaHash(row));
        row.setLastDiscoveredAt(now);
        row.setLastErrorCode(null);
        row.setLastErrorMessage(null);
        row.setUpdateTime(now);
        toolCacheDao().save(row);
        if (oldLocalName != null && !oldLocalName.equals(localName)) {
            toolRegistry.unregister(oldLocalName);
            runtimeToolCache().invalidateLocal(oldLocalName);
            deleteRetiredGovernance(oldLocalName);
        }
        return row;
    }

    private void registerCacheTool(final McpServerConfig server, final McpToolCache cache) {
        final McpToolCache normalizedCache = normalizeCachedLocalToolName(server, cache);
        if (!isDirectlyExposed(normalizedCache)) {
            if (normalizedCache != null && normalizedCache.getLocalToolName() != null) {
                runtimeToolCache().invalidateLocal(normalizedCache.getLocalToolName());
                toolRegistry.unregister(normalizedCache.getLocalToolName());
            }
            return;
        }
        final String localToolName = normalizedCache.getLocalToolName();
        ToolRiskLevel effectiveRisk = normalizedCache.getRiskLevel() == null ? defaultRisk(server) : normalizedCache.getRiskLevel();
        BuiltinToolDefinition def = BuiltinToolDefinition.builder()
                .name(localToolName)
                .displayName(firstNonBlank(normalizedCache.getDisplayName(), localToolName))
                .description(firstNonBlank(normalizedCache.getDescription(), "Remote MCP tool " + normalizedCache.getRemoteToolName()))
                .category(ToolCategory.MCP)
                .mode(ToolExecutionMode.SYNC)
                .risk(effectiveRisk)
                .requireConfirm(Boolean.TRUE.equals(normalizedCache.getRequireConfirm()))
                .sideEffect(sideEffect(effectiveRisk))
                .inputSchema(parseMap(normalizedCache.getInputSchemaJson()))
                .outputSchema(parseMap(normalizedCache.getOutputSchemaJson()))
                .timeoutMs(server.getReadTimeoutMs() == null ? 30000L : server.getReadTimeoutMs().longValue())
                .build();
        ToolExecutor executor = new ToolExecutor() {
            @Override
            public ToolResult execute(ToolContext ctx, Map<String, Object> args) throws Exception {
                long start = System.currentTimeMillis();
                if (ctx != null) ctx.checkCancelled();
                McpRuntimeToolCache.Snapshot snapshot = runtimeToolCache().get(localToolName);
                if (snapshot == null) {
                    throw new LobsterException("mcp.tool_unavailable",
                            "MCP tool is disabled or unavailable: " + localToolName);
                }
                McpServerConfig runtimeServer = snapshot.getServer();
                McpToolCache runtimeCache = snapshot.getTool();
                McpCallOptions options = optionsFromContext(ctx);
                emitMcpCallEvent(ctx, runtimeServer, runtimeCache, options, "started",
                        0L, null, null);
                try {
                    if (ctx != null) ctx.checkCancelled();
                    McpCallResult result = clientFactory.create(runtimeServer).callTool(runtimeServer,
                            runtimeCache.getRemoteToolName(), args, options);
                    long durationMs = System.currentTimeMillis() - start;
                    McpCallLog log = logCall(runtimeServer, runtimeCache, ctx, args, result, durationMs, null);
                    emitMcpCallEvent(ctx, runtimeServer, runtimeCache, options,
                            result != null && result.isError() ? "error" : "ok",
                            durationMs, log, null);
                    Map<String, Object> data = McpClientSupport.dataFromCallResult(result);
                    String message = McpClientSupport.textFromCallResult(result);
                    return result.isError() ? ToolResult.errorData(message, data) : ToolResult.ok(message, data);
                } catch (Exception e) {
                    long durationMs = System.currentTimeMillis() - start;
                    McpCallLog log = logCall(runtimeServer, runtimeCache, ctx, args, null, durationMs, e);
                    emitMcpCallEvent(ctx, runtimeServer, runtimeCache, options,
                            "exception", durationMs, log, e);
                    throw e;
                }
            }
        };
        syncGovernance(def, server);
        toolRegistry.register(def, executor);
    }

    private McpToolCache normalizeCachedLocalToolName(McpServerConfig server, McpToolCache cache) {
        if (server == null || cache == null || cache.getRemoteToolName() == null) return cache;
        String expected = localToolName(server, cache.getRemoteToolName());
        String current = cache.getLocalToolName();
        if (expected.equals(current)) return cache;
        cache.setLocalToolName(expected);
        cache.setUpdateTime(new Date());
        try {
            toolCacheDao().save(cache);
        } catch (Throwable t) {
            try { Tools.log("[McpToolBridge] normalize MCP tool name failed: " + current + " -> " + expected, t); }
            catch (Throwable ignore) { /* ignore */ }
        }
        if (current != null && !current.trim().isEmpty()) {
            toolRegistry.unregister(current);
            runtimeToolCache().invalidateLocal(current);
            deleteRetiredGovernance(current);
        }
        return cache;
    }

    private void syncGovernance(BuiltinToolDefinition def, McpServerConfig server) {
        try {
            ToolDefinitionConfigDao dao = toolDefinitionConfigDao();
            if (dao == null) {
                throw new LobsterException("mcp.governance_unavailable", "Tool governance DAO is unavailable");
            }
            ToolDefinitionConfig row = dao.getByName(def.getToolName());
            boolean isNew = row == null;
            Date now = new Date();
            if (isNew) {
                row = new ToolDefinitionConfig();
                row.setToolName(def.getToolName());
                row.setEnabled(Boolean.TRUE);
                row.setRequireConfirm(def.isRequireConfirm());
                row.setRiskLevel(def.getRiskLevel());
                row.setOrgId(server.getOrgId());
                row.setCreateTime(now);
            }
            row.setDisplayName(def.getDisplayName());
            row.setDescription(def.getDescription());
            row.setCategory(def.getCategory());
            row.setMode(def.getMode());
            row.setMcpServerId(server.getServerId());
            row.setUpdateTime(now);
            dao.save(row);
        } catch (Throwable t) {
            try { Tools.log("[McpToolBridge] sync governance failed: " + def.getToolName(), t); }
            catch (Throwable ignore) { /* ignore */ }
            if (t instanceof LobsterException) {
                throw (LobsterException) t;
            }
            throw new LobsterException("mcp.governance_sync_failed",
                    "MCP tool governance sync failed: " + def.getToolName(), t);
        }
    }

    private void markServerDiscoveryError(McpServerConfig server, Exception error) {
        try {
            List<McpToolCache> rows = toolCacheDao().listByServer(server.getServerId());
            for (McpToolCache row : rows) {
                row.setLastErrorCode("discover_failed");
                row.setLastErrorMessage(truncate(error.getMessage(), 1000));
                row.setUpdateTime(new Date());
                toolCacheDao().save(row);
            }
        } catch (Throwable ignore) { /* best effort */ }
    }

    private McpCallLog logCall(McpServerConfig server, McpToolCache cache, ToolContext ctx,
                               Map<String, Object> args, McpCallResult result,
                               long durationMs, Exception error) {
        try {
            McpCallLogDao dao = mcpCallLogDao();
            if (dao == null) return null;
            Date now = new Date();
            McpCallLog log = new McpCallLog();
            log.setCallId(IdGenerator.prefixed("mcp_"));
            log.setServerId(server.getServerId());
            log.setLocalToolName(cache.getLocalToolName());
            log.setRemoteToolName(cache.getRemoteToolName());
            String requestId = null;
            if (ctx != null) {
                requestId = ctx.getToolCallId() == null ? ctx.getRunId() : ctx.getToolCallId();
                log.setThreadId(ctx.getThreadId());
                log.setRunId(ctx.getRunId());
                log.setToolCallId(ctx.getToolCallId());
                log.setRequestId(requestId);
                log.setUserId(ctx.getUserId());
                log.setOrgId(ctx.getOrgId());
            }
            log.setStatus(error != null ? "exception" : (result != null && result.isError() ? "error" : "ok"));
            log.setDurationMs(Long.valueOf(durationMs));
            Map<String, Object> requestSnapshot = requestSnapshot(server, cache, ctx, requestId, args);
            Map<String, Object> responseSnapshot = responseSnapshot(result, error);
            log.setRequestSummary(truncate(JsonUtil.toJson(requestSummary(args)), 2000));
            log.setResponseSummary(truncate(JsonUtil.toJson(responseSummary(result, error)), 2000));
            String refError = null;
            try {
                FileSystemContentStore store = contentStore();
                if (store != null) {
                    String uid = ctx == null ? null : ctx.getUserId();
                    log.setRequestJsonRef(store.write("mcp-call", uid,
                            JsonUtil.toJson(requestSnapshot), "json"));
                    log.setResponseJsonRef(store.write("mcp-call", uid,
                            JsonUtil.toJson(responseSnapshot), "json"));
                } else {
                    refError = "ContentStore unavailable";
                }
            } catch (Throwable t) {
                refError = t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage();
                try { Tools.log("[McpToolBridge] write MCP call payload ref failed", t); }
                catch (Throwable ignore) { /* ignore */ }
            }
            if (error != null) {
                log.setErrorCode("mcp.call_failed");
                log.setErrorMessage(truncate(error.getMessage(), 1000));
            } else if (result != null && result.isError()) {
                log.setErrorCode("mcp.result_error");
                log.setErrorMessage(truncate(McpClientSupport.textFromCallResult(result), 1000));
            } else if (refError != null) {
                log.setErrorCode("mcp.log_ref_failed");
                log.setErrorMessage(truncate(refError, 1000));
            }
            log.setCreateTime(now);
            dao.save(log);
            updateCallState(cache, result, error, now);
            return log;
        } catch (Throwable t) {
            try { Tools.log("[McpToolBridge] write call log failed", t); } catch (Throwable ignore) { /* ignore */ }
            return null;
        }
    }

    private void emitMcpCallEvent(ToolContext ctx, McpServerConfig server, McpToolCache cache,
                                  McpCallOptions options, String status, long durationMs,
                                  McpCallLog log, Exception error) {
        if (ctx == null || ctx.getProgress() == null) return;
        try {
            Map<String, Object> detail = new LinkedHashMap<>();
            detail.put("event", "mcp_call");
            detail.put("status", status);
            detail.put("serverId", server == null ? null : server.getServerId());
            detail.put("localToolName", cache == null ? null : cache.getLocalToolName());
            detail.put("remoteToolName", cache == null ? null : cache.getRemoteToolName());
            detail.put("schemaHash", cache == null ? null : cache.getSchemaHash());
            detail.put("requestId", options == null ? null : options.getRequestId());
            detail.put("durationMs", Long.valueOf(durationMs));
            if (log != null) {
                detail.put("callId", log.getCallId());
                detail.put("requestJsonRef", log.getRequestJsonRef());
                detail.put("responseJsonRef", log.getResponseJsonRef());
                detail.put("requestSummary", log.getRequestSummary());
                detail.put("responseSummary", log.getResponseSummary());
                detail.put("errorCode", log.getErrorCode());
                detail.put("errorMessage", log.getErrorMessage());
            }
            if (error != null) {
                detail.put("exceptionClass", error.getClass().getSimpleName());
                detail.put("errorMessage", truncate(error.getMessage(), 1000));
            }
            ctx.getProgress().emit("mcp_call", "mcp_call:" + status, detail);
        } catch (Throwable ignore) {
            /* progress must not block tool execution */
        }
    }

    private void updateCallState(McpToolCache cache, McpCallResult result, Exception error, Date now) {
        try {
            if (cache == null) return;
            cache.setLastCallAt(now);
            if (error != null) {
                cache.setLastErrorCode("mcp.call_failed");
                cache.setLastErrorMessage(truncate(error.getMessage(), 1000));
            } else if (result != null && result.isError()) {
                cache.setLastErrorCode("mcp.result_error");
                cache.setLastErrorMessage(truncate(McpClientSupport.textFromCallResult(result), 1000));
            } else {
                cache.setLastErrorCode(null);
                cache.setLastErrorMessage(null);
            }
            cache.setUpdateTime(now);
            toolCacheDao().save(cache);
        } catch (Throwable t) {
            try { Tools.log("[McpToolBridge] update MCP tool call state failed", t); }
            catch (Throwable ignore) { /* ignore */ }
        }
    }

    private Map<String, Object> requestSnapshot(McpServerConfig server, McpToolCache cache,
                                                ToolContext ctx, String requestId,
                                                Map<String, Object> args) {
        Map<String, Object> out = new LinkedHashMap<>();
        if (server != null) {
            out.put("serverId", server.getServerId());
            out.put("endpoint", server.getEndpoint());
            out.put("transportType", server.getTransportType());
        }
        if (cache != null) {
            out.put("toolKey", cache.getToolKey());
            out.put("localToolName", cache.getLocalToolName());
            out.put("remoteToolName", cache.getRemoteToolName());
            out.put("schemaHash", cache.getSchemaHash());
        }
        out.put("requestId", requestId);
        if (ctx != null) {
            out.put("threadId", ctx.getThreadId());
            out.put("runId", ctx.getRunId());
            out.put("toolCallId", ctx.getToolCallId());
            out.put("userId", ctx.getUserId());
            out.put("orgId", ctx.getOrgId());
        }
        out.put("arguments", args == null ? Collections.emptyMap() : args);
        return out;
    }

    private Map<String, Object> responseSnapshot(McpCallResult result, Exception error) {
        Map<String, Object> out = new LinkedHashMap<>();
        if (error != null) {
            out.put("exceptionClass", error.getClass().getName());
            out.put("errorMessage", error.getMessage());
            return out;
        }
        if (result == null) {
            out.put("empty", Boolean.TRUE);
            return out;
        }
        out.put("isError", Boolean.valueOf(result.isError()));
        out.put("message", McpClientSupport.textFromCallResult(result));
        out.put("content", result.getContent());
        out.put("structuredContent", result.getStructuredContent());
        out.put("annotations", result.getAnnotations());
        return out;
    }

    private Map<String, Object> requestSummary(Map<String, Object> args) {
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("argumentKeys", args == null ? Collections.emptyList() : new ArrayList<>(args.keySet()));
        out.put("argumentCount", Integer.valueOf(args == null ? 0 : args.size()));
        return out;
    }

    private Map<String, Object> responseSummary(McpCallResult result, Exception error) {
        Map<String, Object> out = new LinkedHashMap<>();
        if (error != null) {
            out.put("status", "exception");
            out.put("exceptionClass", error.getClass().getSimpleName());
            out.put("message", truncate(error.getMessage(), 300));
            return out;
        }
        if (result == null) {
            out.put("status", "empty");
            return out;
        }
        out.put("status", result.isError() ? "error" : "ok");
        out.put("message", truncate(McpClientSupport.textFromCallResult(result), 300));
        out.put("contentCount", Integer.valueOf(result.getContent() == null ? 0 : result.getContent().size()));
        out.put("hasStructuredContent", Boolean.valueOf(result.getStructuredContent() != null));
        out.put("annotationKeys", result.getAnnotations() == null
                ? Collections.emptyList() : new ArrayList<>(result.getAnnotations().keySet()));
        return out;
    }

    private McpCallOptions defaultOptions(String action) {
        McpCallOptions options = new McpCallOptions();
        options.setRequestId("mcp-" + action + "-" + System.currentTimeMillis());
        return options;
    }

    private McpCallOptions optionsFromContext(ToolContext ctx) {
        McpCallOptions options = new McpCallOptions();
        if (ctx != null) {
            options.setRequestId(ctx.getToolCallId() == null ? ctx.getRunId() : ctx.getToolCallId());
            options.setThreadId(ctx.getThreadId());
            options.setRunId(ctx.getRunId());
            options.setToolCallId(ctx.getToolCallId());
            options.setUserId(ctx.getUserId());
            options.setOrgId(ctx.getOrgId());
            long timeoutMs = ctx.getDeadlineAtMs() > 0 ? Math.max(0, ctx.getDeadlineAtMs() - System.currentTimeMillis()) : 0;
            options.setTimeoutMs(timeoutMs);
        }
        return options;
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> parseMap(String json) {
        if (json == null || json.trim().isEmpty()) return Collections.emptyMap();
        try {
            Object parsed = JsonUtil.fromJson(json, Object.class);
            if (parsed instanceof Map) return (Map<String, Object>) parsed;
        } catch (Throwable ignore) { /* fallback */ }
        return Collections.emptyMap();
    }

    private String localToolName(McpServerConfig server, String remoteName) {
        return McpToolNameMapper.localToolName(server, remoteName);
    }

    private boolean isDirectlyExposed(McpToolCache cache) {
        return cache != null
                && !Boolean.FALSE.equals(cache.getEnabled())
                && cache.getExposureMode() == McpToolExposureMode.DIRECT;
    }

    private void deleteRetiredGovernance(String oldLocalName) {
        try {
            ToolDefinitionConfigDao dao = toolDefinitionConfigDao();
            if (dao != null) dao.deleteByName(oldLocalName);
        } catch (Throwable t) {
            try { Tools.log("[McpToolBridge] delete retired MCP governance failed: " + oldLocalName, t); }
            catch (Throwable ignore) { /* ignore */ }
        }
    }

    private SideEffectLevel sideEffect(ToolRiskLevel risk) {
        if (risk == null || risk == ToolRiskLevel.READ_ONLY) return SideEffectLevel.READ;
        return risk == ToolRiskLevel.WRITE ? SideEffectLevel.WRITE_EXTERNAL : SideEffectLevel.WRITE_EXTERNAL;
    }

    private ToolRiskLevel defaultRisk(McpServerConfig server) {
        return server == null || server.getDefaultRisk() == null ? ToolRiskLevel.READ_ONLY : server.getDefaultRisk();
    }

    private String schemaHash(McpToolCache row) {
        String raw = String.valueOf(row.getInputSchemaJson()) + "\n"
                + String.valueOf(row.getOutputSchemaJson()) + "\n"
                + String.valueOf(row.getAnnotationsJson());
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] bytes = digest.digest(raw.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder("sha256:");
            for (byte b : bytes) sb.append(String.format("%02x", b & 0xff));
            return sb.toString();
        } catch (Throwable t) {
            return "sha256:unavailable";
        }
    }

    private String firstNonBlank(String a, String b) {
        return a == null || a.trim().isEmpty() ? b : a;
    }

    private String truncate(String s, int max) {
        if (s == null || s.length() <= max) return s;
        return s.substring(0, max);
    }

    private McpServerConfigDao serverConfigDao() {
        try {
            McpServerConfigDao d = Tools.getBean(McpServerConfigDao.class);
            if (d != null) return d;
        } catch (Throwable ignore) { /* fallback */ }
        return serverConfigDao;
    }

    private McpToolCacheDao toolCacheDao() {
        try {
            McpToolCacheDao d = Tools.getBean(McpToolCacheDao.class);
            if (d != null) return d;
        } catch (Throwable ignore) { /* fallback */ }
        return toolCacheDao;
    }

    private ToolDefinitionConfigDao toolDefinitionConfigDao() {
        try {
            ToolDefinitionConfigDao d = Tools.getBean(ToolDefinitionConfigDao.class);
            if (d != null) return d;
        } catch (Throwable ignore) { /* fallback */ }
        return toolDefinitionConfigDao;
    }

    private McpCallLogDao mcpCallLogDao() {
        try {
            McpCallLogDao d = Tools.getBean(McpCallLogDao.class);
            if (d != null) return d;
        } catch (Throwable ignore) { /* fallback */ }
        return mcpCallLogDao;
    }

    private FileSystemContentStore contentStore() {
        try {
            FileSystemContentStore s = Tools.getBean(FileSystemContentStore.class);
            if (s != null) return s;
        } catch (Throwable ignore) { /* fallback */ }
        return contentStore;
    }

    private McpRuntimeToolCache runtimeToolCache() {
        try {
            McpRuntimeToolCache c = Tools.getBean(McpRuntimeToolCache.class);
            if (c != null) return c;
        } catch (Throwable ignore) { /* fallback */ }
        return runtimeToolCache == null ? new McpRuntimeToolCache() : runtimeToolCache;
    }

    private long mcpToolListTtlMs() {
        try {
            long configured = LobsterConfig.getMcpToolListTtlMs();
            return configured > 0 ? configured : DEFAULT_REFRESH_MS;
        } catch (Throwable ignore) {
            return DEFAULT_REFRESH_MS;
        }
    }

    public static class DiscoverSummary {
        public String serverId;
        public int discovered;
        public int registered;
        public int disabled;
        public List<String> errors = new ArrayList<>();

        public Map<String, Object> toMap() {
            Map<String, Object> out = new LinkedHashMap<>();
            out.put("serverId", serverId);
            out.put("discovered", discovered);
            out.put("registered", registered);
            out.put("disabled", disabled);
            out.put("errors", errors);
            return out;
        }
    }
}
