import { spawn } from 'child_process';
import { EventEmitter } from 'events';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { getErrorMessage } from '../types/index.js';
import { logger } from '../utils/logger.js';
export class MCPClient extends EventEmitter {
    servers = new Map();
    configPath;
    constructor() {
        super();
        this.configPath = path.join(process.cwd(), '.codebuddy', 'mcp-servers.json');
    }
    /**
     * Load MCP server configurations from file
     */
    loadConfig() {
        // Check project-level config first
        if (fs.existsSync(this.configPath)) {
            try {
                const content = fs.readFileSync(this.configPath, 'utf-8');
                const config = JSON.parse(content);
                return config.servers || [];
            }
            catch (error) {
                logger.error(`Failed to load MCP config: ${error instanceof Error ? error.message : String(error)}`);
            }
        }
        // Check user-level config
        const userConfigPath = path.join(os.homedir(), '.codebuddy', 'mcp-servers.json');
        if (fs.existsSync(userConfigPath)) {
            try {
                const content = fs.readFileSync(userConfigPath, 'utf-8');
                const config = JSON.parse(content);
                return config.servers || [];
            }
            catch (error) {
                logger.error(`Failed to load user MCP config: ${error instanceof Error ? error.message : String(error)}`);
            }
        }
        return [];
    }
    /**
     * Save MCP server configuration
     */
    saveConfig(servers) {
        const dir = path.dirname(this.configPath);
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        fs.writeFileSync(this.configPath, JSON.stringify({ servers }, null, 2));
    }
    /**
     * Connect to all configured servers
     */
    async connectAll() {
        const configs = this.loadConfig();
        // Connect to all enabled servers in parallel for faster startup
        const enabledConfigs = configs.filter(config => config.enabled !== false);
        const results = await Promise.allSettled(enabledConfigs.map(config => this.connect(config)));
        // Log connection failures
        results.forEach((result, index) => {
            if (result.status === 'rejected') {
                logger.error(`Failed to connect to MCP server ${enabledConfigs[index].name}: ${getErrorMessage(result.reason)}`);
            }
        });
    }
    /**
     * Connect to a specific MCP server
     */
    async connect(config) {
        if (this.servers.has(config.name)) {
            throw new Error(`MCP server "${config.name}" is already connected. Disconnect it first with disconnect("${config.name}") before reconnecting.`);
        }
        const connection = new MCPServerConnection(config);
        await connection.start();
        this.servers.set(config.name, connection);
        this.emit('server-connected', config.name);
    }
    /**
     * Disconnect from a server
     */
    async disconnect(serverName) {
        const server = this.servers.get(serverName);
        if (server) {
            await server.stop();
            this.servers.delete(serverName);
            this.emit('server-disconnected', serverName);
        }
    }
    /**
     * Disconnect from all servers
     */
    async disconnectAll() {
        // Disconnect from all servers in parallel for faster shutdown
        const serverNames = Array.from(this.servers.keys());
        await Promise.allSettled(serverNames.map(name => this.disconnect(name)));
    }
    /**
     * Dispose and cleanup all resources
     */
    async dispose() {
        await this.disconnectAll();
        this.removeAllListeners();
    }
    /**
     * Get all available tools from connected servers
     */
    async getAllTools() {
        const allTools = new Map();
        const serverEntries = Array.from(this.servers.entries());
        // Fetch tools from all servers in parallel for better performance
        const results = await Promise.allSettled(serverEntries.map(async ([name, server]) => {
            const tools = await server.listTools();
            return { name, tools };
        }));
        for (const result of results) {
            if (result.status === 'fulfilled') {
                allTools.set(result.value.name, result.value.tools);
            }
            else {
                // Find the server name for error logging
                const index = results.indexOf(result);
                const serverName = serverEntries[index]?.[0] || 'unknown';
                logger.error(`Failed to get tools from ${serverName}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
            }
        }
        return allTools;
    }
    /**
     * Get all available resources from connected servers
     */
    async getAllResources() {
        const allResources = new Map();
        const serverEntries = Array.from(this.servers.entries());
        // Fetch resources from all servers in parallel for better performance
        const results = await Promise.allSettled(serverEntries.map(async ([name, server]) => {
            const resources = await server.listResources();
            return { name, resources };
        }));
        for (const result of results) {
            if (result.status === 'fulfilled') {
                allResources.set(result.value.name, result.value.resources);
            }
            else {
                // Find the server name for error logging
                const index = results.indexOf(result);
                const serverName = serverEntries[index]?.[0] || 'unknown';
                logger.error(`Failed to get resources from ${serverName}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
            }
        }
        return allResources;
    }
    /**
     * Call a tool on a specific server
     */
    async callTool(serverName, toolName, args) {
        const server = this.servers.get(serverName);
        if (!server) {
            throw new Error(`MCP server "${serverName}" is not connected. Use connect() to establish a connection first, or check getConnectedServers() for available servers.`);
        }
        return server.callTool(toolName, args);
    }
    /**
     * Read a resource from a specific server
     */
    async readResource(serverName, uri) {
        const server = this.servers.get(serverName);
        if (!server) {
            throw new Error(`MCP server "${serverName}" is not connected. Use connect() to establish a connection first, or check getConnectedServers() for available servers.`);
        }
        return server.readResource(uri);
    }
    /**
     * Get list of connected servers
     */
    getConnectedServers() {
        return Array.from(this.servers.keys());
    }
    /**
     * Check if a server is connected
     */
    isConnected(serverName) {
        return this.servers.has(serverName);
    }
    /**
     * Format status for display
     */
    formatStatus() {
        const servers = this.getConnectedServers();
        if (servers.length === 0) {
            return 'No MCP servers connected.\nConfigure servers in .codebuddy/mcp-servers.json';
        }
        return `Connected MCP Servers:\n${servers.map(s => `  • ${s}`).join('\n')}`;
    }
}
/**
 * Connection to a single MCP server
 */
class MCPServerConnection extends EventEmitter {
    config;
    process = null;
    requestId = 0;
    pendingRequests = new Map();
    buffer = '';
    constructor(config) {
        super();
        this.config = config;
    }
    async start() {
        return new Promise((resolve, reject) => {
            const env = { ...process.env, ...this.config.env };
            this.process = spawn(this.config.command, this.config.args || [], {
                env,
                stdio: ['pipe', 'pipe', 'pipe']
            });
            this.process.stdout?.on('data', (data) => {
                this.handleData(data.toString());
            });
            this.process.stderr?.on('data', (data) => {
                logger.debug(`[MCP:${this.config.name}] ${data.toString().trim()}`);
            });
            this.process.on('error', (error) => {
                reject(error);
            });
            this.process.on('close', (code) => {
                this.emit('close', code);
            });
            // Initialize the connection
            this.initialize()
                .then(() => resolve())
                .catch(reject);
        });
    }
    async stop() {
        if (this.process) {
            this.process.kill();
            this.process = null;
        }
    }
    async initialize() {
        await this.sendRequest('initialize', {
            protocolVersion: '2024-11-05',
            capabilities: {
                tools: {},
                resources: {}
            },
            clientInfo: {
                name: 'code-buddy',
                version: '1.0.0'
            }
        });
        await this.sendNotification('notifications/initialized', {});
    }
    handleData(data) {
        this.buffer += data;
        // Try to parse complete JSON-RPC messages
        const lines = this.buffer.split('\n');
        this.buffer = lines.pop() || '';
        for (const line of lines) {
            if (line.trim()) {
                try {
                    const message = JSON.parse(line);
                    this.handleMessage(message);
                }
                catch (error) {
                    logger.error(`Failed to parse MCP message: ${error instanceof Error ? error.message : String(error)}`);
                }
            }
        }
    }
    handleMessage(message) {
        const pending = this.pendingRequests.get(message.id);
        if (pending) {
            this.pendingRequests.delete(message.id);
            if (message.error) {
                pending.reject(new Error(message.error.message));
            }
            else {
                pending.resolve(message.result);
            }
        }
    }
    async sendRequest(method, params) {
        return new Promise((resolve, reject) => {
            const id = ++this.requestId;
            const request = {
                jsonrpc: '2.0',
                id,
                method,
                params
            };
            this.pendingRequests.set(id, { resolve, reject });
            if (this.process?.stdin) {
                this.process.stdin.write(JSON.stringify(request) + '\n');
            }
            else {
                reject(new Error('MCP server process not started. The server command may have failed to launch or exited unexpectedly.'));
            }
            // Timeout after 30 seconds
            setTimeout(() => {
                if (this.pendingRequests.has(id)) {
                    this.pendingRequests.delete(id);
                    reject(new Error('MCP request timed out after 30 seconds. The server may be unresponsive or processing a long operation.'));
                }
            }, 30000);
        });
    }
    sendNotification(method, params) {
        const notification = {
            jsonrpc: '2.0',
            method,
            params
        };
        if (this.process?.stdin) {
            this.process.stdin.write(JSON.stringify(notification) + '\n');
        }
    }
    async listTools() {
        const result = await this.sendRequest('tools/list', {});
        return result.tools || [];
    }
    async listResources() {
        const result = await this.sendRequest('resources/list', {});
        return result.resources || [];
    }
    async callTool(name, args) {
        const result = await this.sendRequest('tools/call', {
            name,
            arguments: args
        });
        return result;
    }
    async readResource(uri) {
        const result = await this.sendRequest('resources/read', { uri });
        return result;
    }
}
// Singleton instance
let mcpClientInstance = null;
export function getMCPClient() {
    if (!mcpClientInstance) {
        mcpClientInstance = new MCPClient();
    }
    return mcpClientInstance;
}
export async function resetMCPClient() {
    if (mcpClientInstance) {
        await mcpClientInstance.dispose();
    }
    mcpClientInstance = null;
}
//# sourceMappingURL=mcp-client.js.map