"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.getClients = getClients;
exports.getBuiltinFactories = getBuiltinFactories;
exports.getBuiltinServers = getBuiltinServers;
exports.registerBuiltinServer = registerBuiltinServer;
exports.connectToServer = connectToServer;
exports.disconnectFromServer = disconnectFromServer;
exports.resolveClient = resolveClient;
exports.connectAutoServers = connectAutoServers;
exports.disconnectAll = disconnectAll;
exports.isConnected = isConnected;
exports.clear = clear;
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
const inMemory_js_1 = require("@modelcontextprotocol/sdk/inMemory.js");
const logger_1 = require("../../../lib/logger");
const mcpDbService = __importStar(require("../db"));
/** In-memory map of server ID/name to active MCP client */
const clients = new Map();
/**
 * Built-in server factories -- keyed by server name.
 * These are in-process MCP servers that connect via InMemoryTransport
 * rather than external stdio/SSE connections.
 */
const builtinFactories = new Map();
/**
 * Cached built-in McpServer instances -- keyed by canonical server name.
 * Used by dispatchBuiltinTool() to call tool handlers directly without
 * going through MCP Client/Transport. One instance per server.
 */
const builtinServers = new Map();
/** Read-only access to the clients map for sibling modules. */
function getClients() {
    return clients;
}
/** Read-only access to the builtin factories map for sibling modules. */
function getBuiltinFactories() {
    return builtinFactories;
}
/** Read-only access to the builtin servers map for sibling modules. */
function getBuiltinServers() {
    return builtinServers;
}
/**
 * Register a built-in server factory so it can be auto-connected
 * when callServerTool is invoked with its name.
 */
function registerBuiltinServer(name, factory) {
    builtinFactories.set(name, factory);
}
/**
 * Connect to a registered MCP server.
 * Creates the appropriate transport based on transport_type,
 * connects, and caches tool manifest in DB.
 */
async function connectToServer(server) {
    if (clients.has(server.id)) {
        return clients.get(server.id);
    }
    // Built-in servers use InMemoryTransport via their registered factory
    if (server.transport_config?.builtin) {
        // Find matching factory by server name
        for (const [name, factory] of builtinFactories) {
            if (server.name === name || name.includes(server.name) || server.name.includes(name)) {
                // Reuse existing client if factory was already connected
                if (clients.has(name)) {
                    clients.set(server.id, clients.get(name));
                    await mcpDbService.updateMcpServerStatus(server.id, 'connected');
                    return clients.get(name);
                }
                const srv = await factory();
                const [clientTransport, serverTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
                await srv.connect(serverTransport);
                const client = new index_js_1.Client({ name: `builtin-${name}`, version: '1.0.0' });
                await client.connect(clientTransport);
                clients.set(name, client);
                clients.set(server.id, client);
                // Cache the tool manifest
                const { tools } = await client.listTools();
                const manifest = tools.map((t) => ({
                    name: t.name,
                    description: t.description || '',
                    inputSchema: t.inputSchema || {},
                }));
                await mcpDbService.updateMcpServerStatus(server.id, 'connected', manifest);
                logger_1.loggerRegistry.info(`[lt-mcp:client] connected builtin server: ${name} (${manifest.length} tools)`);
                return client;
            }
        }
        throw new Error(`No builtin factory registered for server: ${server.name}`);
    }
    const client = new index_js_1.Client({ name: 'long-tail', version: '1.0.0' });
    let transport;
    const ttype = server.transport_type;
    if (ttype === 'stdio') {
        transport = new stdio_js_1.StdioClientTransport({
            command: server.transport_config.command,
            args: server.transport_config.args || [],
            env: server.transport_config.env,
        });
    }
    else if (ttype === 'streamable-http') {
        transport = new streamableHttp_js_1.StreamableHTTPClientTransport(new URL(server.transport_config.url));
    }
    else {
        transport = new sse_js_1.SSEClientTransport(new URL(server.transport_config.url));
    }
    await client.connect(transport);
    clients.set(server.id, client);
    // Cache the tool manifest
    const { tools } = await client.listTools();
    const manifest = tools.map((t) => ({
        name: t.name,
        description: t.description || '',
        inputSchema: t.inputSchema || {},
    }));
    await mcpDbService.updateMcpServerStatus(server.id, 'connected', manifest);
    logger_1.loggerRegistry.info(`[lt-mcp:client] connected to ${server.name} (${manifest.length} tools)`);
    return client;
}
/**
 * Disconnect from a specific server.
 */
async function disconnectFromServer(serverId) {
    const client = clients.get(serverId);
    if (client) {
        await client.close();
        clients.delete(serverId);
        await mcpDbService.updateMcpServerStatus(serverId, 'disconnected');
        logger_1.loggerRegistry.info(`[lt-mcp:client] disconnected from ${serverId}`);
    }
}
/**
 * Resolve a server by ID or name, auto-connecting built-in servers if needed.
 * Returns the client or null if not found.
 *
 * Built-in servers are connected once under their canonical factory name.
 * Alias lookups (e.g. 'translation' matching 'long-tail-translation') reuse
 * the same client instance to avoid double-connecting the singleton server.
 */
async function resolveClient(serverId) {
    // 1. Direct lookup (by UUID or name)
    if (clients.has(serverId))
        return clients.get(serverId);
    // 2. Check built-in server factories -- exact match first, then fuzzy
    //    Normalize strips non-alphanumeric chars so hyphens and underscores
    //    match (e.g., "long_tail_vision" matches "long-tail-vision").
    const norm = (s) => s.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
    let matchedName = null;
    if (builtinFactories.has(serverId)) {
        matchedName = serverId;
    }
    else {
        const normId = norm(serverId);
        for (const [name] of builtinFactories) {
            const normName = norm(name);
            if (normName.includes(normId) || normId.includes(normName)) {
                matchedName = name;
                break;
            }
        }
    }
    if (matchedName) {
        const factory = builtinFactories.get(matchedName);
        // Check if we already connected this factory under its canonical name
        if (clients.has(matchedName)) {
            // Alias the serverId to the existing client so future lookups are instant
            clients.set(serverId, clients.get(matchedName));
            return clients.get(matchedName);
        }
        const server = await factory();
        const [clientTransport, serverTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
        await server.connect(serverTransport);
        const client = new index_js_1.Client({ name: `builtin-${matchedName}`, version: '1.0.0' });
        await client.connect(clientTransport);
        // Cache under both the canonical name and the requested serverId
        clients.set(matchedName, client);
        if (serverId !== matchedName)
            clients.set(serverId, client);
        logger_1.loggerRegistry.info(`[lt-mcp:client] auto-connected built-in server: ${matchedName} (as '${serverId}')`);
        return client;
    }
    // 3. Look up in DB by ID or name, then try to match a built-in factory
    try {
        const dbServer = (await mcpDbService.getMcpServer(serverId)) ||
            (await mcpDbService.getMcpServerByName(serverId));
        if (dbServer) {
            // Already connected under its DB id?
            if (clients.has(dbServer.id)) {
                clients.set(serverId, clients.get(dbServer.id));
                return clients.get(dbServer.id);
            }
            // Match DB server name to a built-in factory
            for (const [name, factory] of builtinFactories) {
                if (dbServer.name === name || name.includes(dbServer.name) || dbServer.name.includes(name)) {
                    if (clients.has(name)) {
                        clients.set(serverId, clients.get(name));
                        return clients.get(name);
                    }
                    const srv = await factory();
                    const [clientTransport, serverTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
                    await srv.connect(serverTransport);
                    const client = new index_js_1.Client({ name: `builtin-${name}`, version: '1.0.0' });
                    await client.connect(clientTransport);
                    clients.set(name, client);
                    clients.set(dbServer.id, client);
                    if (serverId !== name && serverId !== dbServer.id)
                        clients.set(serverId, client);
                    logger_1.loggerRegistry.info(`[lt-mcp:client] auto-connected built-in server: ${name} (via DB id '${serverId}')`);
                    return client;
                }
            }
        }
    }
    catch {
        // DB lookup failed -- not critical
    }
    return null;
}
/**
 * Connect to all auto-connect servers.
 */
async function connectAutoServers() {
    const servers = await mcpDbService.getAutoConnectServers();
    for (const server of servers) {
        // Skip built-in servers -- they auto-connect lazily via resolveClient()
        // on first tool call using InMemoryTransport, not stdio/SSE.
        if (server.transport_config?.builtin) {
            continue;
        }
        try {
            await connectToServer(server);
        }
        catch (err) {
            logger_1.loggerRegistry.error(`[lt-mcp:client] failed to connect to ${server.name}: ${err.message}`);
            await mcpDbService.updateMcpServerStatus(server.id, 'error');
        }
    }
}
/**
 * Disconnect all clients.
 */
async function disconnectAll() {
    for (const [serverId] of clients) {
        await disconnectFromServer(serverId);
    }
}
/**
 * Check if a server is connected.
 */
function isConnected(serverId) {
    return clients.has(serverId);
}
/**
 * Clear all state. Used in tests.
 */
function clear() {
    clients.clear();
    builtinServers.clear();
}
