/**
 * MCP Client - Connection management for MCP servers
 */
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { debug, filterTools, getConcurrencyLimit, getMaxRetries, getRetryDelayMs, getTimeoutMs, isDaemonEnabled, isHttpServer, isToolAllowed, } from './config.js';
import { cleanupOrphanedDaemons, getDaemonConnection, } from './daemon-client.js';
import { VERSION } from './version.js';
// Re-export config utilities for convenience
export { debug, getTimeoutMs, getConcurrencyLimit };
/**
 * Get retry config respecting MCP_TIMEOUT budget
 */
function getRetryConfig() {
    const totalBudgetMs = getTimeoutMs();
    const maxRetries = getMaxRetries();
    const baseDelayMs = getRetryDelayMs();
    // Reserve at least 5s for the final attempt
    const retryBudgetMs = Math.max(0, totalBudgetMs - 5000);
    return {
        maxRetries,
        baseDelayMs,
        maxDelayMs: Math.min(10000, retryBudgetMs / 2),
        totalBudgetMs,
    };
}
/**
 * Check if an error is transient and worth retrying
 * Uses error codes when available, falls back to message matching
 */
export function isTransientError(error) {
    // Check error code first (more reliable than message matching)
    const nodeError = error;
    if (nodeError.code) {
        const transientCodes = [
            'ECONNREFUSED',
            'ECONNRESET',
            'ETIMEDOUT',
            'ENOTFOUND',
            'EPIPE',
            'ENETUNREACH',
            'EHOSTUNREACH',
            'EAI_AGAIN',
        ];
        if (transientCodes.includes(nodeError.code)) {
            return true;
        }
    }
    // Fallback to message matching for errors without codes
    const message = error.message;
    // HTTP transient errors - require status code at start or with HTTP context
    // Pattern: "502", "502 Bad Gateway", "HTTP 502", "status 502", "status code 502"
    if (/^(502|503|504|429)\b/.test(message))
        return true;
    if (/\b(http|status(\s+code)?)\s*(502|503|504|429)\b/i.test(message))
        return true;
    if (/\b(502|503|504|429)\s+(bad gateway|service unavailable|gateway timeout|too many requests)/i.test(message))
        return true;
    // Generic network terms - more specific patterns
    if (/network\s*(error|fail|unavailable|timeout)/i.test(message))
        return true;
    if (/connection\s*(reset|refused|timeout)/i.test(message))
        return true;
    if (/\btimeout\b/i.test(message))
        return true;
    return false;
}
/**
 * Calculate delay with exponential backoff and jitter
 */
function calculateDelay(attempt, config) {
    const exponentialDelay = config.baseDelayMs * 2 ** attempt;
    const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
    // Add jitter (±25%)
    const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
    return Math.round(cappedDelay + jitter);
}
/**
 * Sleep for specified milliseconds
 */
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
 * Execute a function with retry logic for transient failures
 * Respects overall timeout budget from MCP_TIMEOUT
 */
async function withRetry(fn, operationName, config = getRetryConfig()) {
    let lastError;
    const startTime = Date.now();
    for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
        // Check if we've exceeded the total timeout budget
        const elapsed = Date.now() - startTime;
        if (elapsed >= config.totalBudgetMs) {
            debug(`${operationName}: timeout budget exhausted after ${elapsed}ms`);
            break;
        }
        try {
            return await fn();
        }
        catch (error) {
            lastError = error;
            const remainingBudget = config.totalBudgetMs - (Date.now() - startTime);
            const shouldRetry = attempt < config.maxRetries &&
                isTransientError(lastError) &&
                remainingBudget > 1000; // At least 1s remaining
            if (shouldRetry) {
                const delay = Math.min(calculateDelay(attempt, config), remainingBudget - 1000);
                debug(`${operationName} failed (attempt ${attempt + 1}/${config.maxRetries + 1}): ${lastError.message}. Retrying in ${delay}ms...`);
                await sleep(delay);
            }
            else {
                throw lastError;
            }
        }
    }
    throw lastError;
}
/**
 * Safely close a connection, logging but not throwing on error
 */
export async function safeClose(close) {
    try {
        await close();
    }
    catch (err) {
        debug(`Failed to close connection: ${err.message}`);
    }
}
/**
 * Connect to an MCP server with retry logic
 * Captures stderr from stdio servers to include in error messages
 */
export async function connectToServer(serverName, config) {
    // Collect stderr for better error messages
    const stderrChunks = [];
    return withRetry(async () => {
        const client = new Client({
            name: 'mcp-cli',
            version: VERSION,
        }, {
            capabilities: {},
        });
        let transport;
        if (isHttpServer(config)) {
            transport = createHttpTransport(config);
        }
        else {
            transport = createStdioTransport(config);
            // Capture stderr for debugging - attach BEFORE connect
            // Always stream stderr immediately so auth prompts are visible
            const stderrStream = transport.stderr;
            if (stderrStream) {
                stderrStream.on('data', (chunk) => {
                    const text = chunk.toString();
                    stderrChunks.push(text);
                    // Always stream stderr immediately so users can see auth prompts
                    process.stderr.write(`[${serverName}] ${text}`);
                });
            }
        }
        try {
            await client.connect(transport);
        }
        catch (error) {
            // Enhance error with captured stderr
            const stderrOutput = stderrChunks.join('').trim();
            if (stderrOutput) {
                const err = error;
                err.message = `${err.message}\n\nServer stderr:\n${stderrOutput}`;
            }
            throw error;
        }
        // For successful connections, forward stderr to console
        if (!isHttpServer(config)) {
            const stderrStream = transport.stderr;
            if (stderrStream) {
                stderrStream.on('data', (chunk) => {
                    process.stderr.write(chunk);
                });
            }
        }
        return {
            client,
            close: async () => {
                await client.close();
            },
        };
    }, `connect to ${serverName}`);
}
/**
 * Create HTTP transport for remote servers
 */
function createHttpTransport(config) {
    const url = new URL(config.url);
    return new StreamableHTTPClientTransport(url, {
        requestInit: {
            headers: config.headers,
        },
    });
}
/**
 * Create stdio transport for local servers
 * Uses stderr: 'pipe' to capture server output for debugging
 */
function createStdioTransport(config) {
    // Merge process.env with config.env, filtering out undefined values
    const mergedEnv = {};
    for (const [key, value] of Object.entries(process.env)) {
        if (value !== undefined) {
            mergedEnv[key] = value;
        }
    }
    if (config.env) {
        Object.assign(mergedEnv, config.env);
    }
    return new StdioClientTransport({
        command: config.command,
        args: config.args,
        env: mergedEnv,
        cwd: config.cwd,
        stderr: 'pipe', // Capture stderr for better error messages
    });
}
/**
 * List all tools from a connected client with retry logic
 */
export async function listTools(client) {
    return withRetry(async () => {
        const result = await client.listTools();
        return result.tools.map((tool) => ({
            name: tool.name,
            description: tool.description,
            inputSchema: tool.inputSchema,
        }));
    }, 'list tools');
}
/**
 * Get a specific tool by name
 */
export async function getTool(client, toolName) {
    const tools = await listTools(client);
    return tools.find((t) => t.name === toolName);
}
/**
 * Call a tool with arguments and retry logic
 */
export async function callTool(client, toolName, args) {
    return withRetry(async () => {
        const result = await client.callTool({
            name: toolName,
            arguments: args,
        }, undefined, { timeout: getTimeoutMs() });
        return result;
    }, `call tool ${toolName}`);
}
// ============================================================================
// Unified Connection Interface (Daemon + Direct)
// ============================================================================
/**
 * Get a unified connection to an MCP server
 *
 * If daemon mode is enabled (default), tries to use a cached daemon connection.
 * Falls back to direct connection if daemon fails or is disabled.
 *
 * @param serverName - Name of the server from config
 * @param config - Server configuration
 * @returns McpConnection with listTools, callTool, and close methods
 */
export async function getConnection(serverName, config) {
    // Clean up any orphaned daemons on first call
    await cleanupOrphanedDaemons();
    // Try daemon connection if enabled
    if (isDaemonEnabled()) {
        try {
            const daemonConn = await getDaemonConnection(serverName, config);
            if (daemonConn) {
                debug(`Using daemon connection for ${serverName}`);
                return {
                    async listTools() {
                        const data = await daemonConn.listTools();
                        const tools = data;
                        // Apply tool filtering from config
                        return filterTools(tools, config);
                    },
                    async callTool(toolName, args) {
                        // Check if tool is allowed before calling
                        if (!isToolAllowed(toolName, config)) {
                            throw new Error(`Tool "${toolName}" is disabled by configuration`);
                        }
                        return daemonConn.callTool(toolName, args);
                    },
                    async getInstructions() {
                        return daemonConn.getInstructions();
                    },
                    async close() {
                        await daemonConn.close();
                    },
                    isDaemon: true,
                };
            }
        }
        catch (err) {
            debug(`Daemon connection failed for ${serverName}: ${err.message}, falling back to direct`);
        }
    }
    // Fall back to direct connection
    debug(`Using direct connection for ${serverName}`);
    const { client, close } = await connectToServer(serverName, config);
    return {
        async listTools() {
            const tools = await listTools(client);
            // Apply tool filtering from config
            return filterTools(tools, config);
        },
        async callTool(toolName, args) {
            // Check if tool is allowed before calling
            if (!isToolAllowed(toolName, config)) {
                throw new Error(`Tool "${toolName}" is disabled by configuration`);
            }
            return callTool(client, toolName, args);
        },
        async getInstructions() {
            return client.getInstructions();
        },
        async close() {
            await close();
        },
        isDaemon: false,
    };
}
