import { spawn } from 'child_process';
import { unlink } from 'fs/promises';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { getSocketPath, waitForFile, isProcessAlive, fileExists, getLogsDir } from './utils.js';
import { updateSession, getSession } from './sessions.js';
import { createLogger } from './logger.js';
import { ClientError, NetworkError, isAuthenticationError, createServerAuthError } from './errors.js';
import { BridgeClient } from './bridge-client.js';
import { readKeychainOAuthTokenInfo, readKeychainOAuthClientInfo, readKeychainSessionHeaders } from './auth/keychain.js';
import { getAuthProfile } from './auth/profiles.js';
const logger = createLogger('bridge-manager');
function getBridgeExecutable() {
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = dirname(__filename);
    return join(__dirname, '..', 'bridge', 'index.js');
}
export async function startBridge(options) {
    const { sessionName, serverConfig, verbose, profileName, headers, proxyConfig, mcpSessionId } = options;
    logger.debug(`Launching bridge for session: ${sessionName}`);
    const socketPath = getSocketPath(sessionName);
    if (process.platform !== 'win32' && (await fileExists(socketPath))) {
        logger.debug(`Removing existing socket: ${socketPath}`);
        await unlink(socketPath);
    }
    const sanitizedTarget = { ...serverConfig };
    delete sanitizedTarget.headers;
    const bridgeExecutable = getBridgeExecutable();
    const targetJson = JSON.stringify(sanitizedTarget);
    const args = [sessionName, targetJson];
    if (verbose) {
        args.push('--verbose');
    }
    if (profileName) {
        args.push('--profile', profileName);
    }
    else if (headers && Object.keys(headers).length > 0) {
        args.push('--profile', 'dummy');
    }
    if (proxyConfig) {
        args.push('--proxy-host', proxyConfig.host);
        args.push('--proxy-port', String(proxyConfig.port));
    }
    if (mcpSessionId) {
        args.push('--mcp-session-id', mcpSessionId);
        logger.debug(`Passing MCP session ID for resumption: ${mcpSessionId}`);
    }
    logger.debug('Bridge executable:', bridgeExecutable);
    logger.debug('Bridge args:', args);
    const bridgeProcess = spawn('node', [bridgeExecutable, ...args], {
        detached: true,
        stdio: 'ignore',
    });
    bridgeProcess.unref();
    logger.debug(`Bridge process spawned with PID: ${bridgeProcess.pid}`);
    if (!bridgeProcess.pid) {
        throw new ClientError('Failed to spawn bridge process: no PID');
    }
    const pid = bridgeProcess.pid;
    try {
        await waitForFile(socketPath, { timeoutMs: 5000 });
    }
    catch {
        try {
            process.kill(pid, 'SIGTERM');
        }
        catch {
        }
        throw new ClientError(`Bridge failed to start: socket file not created within timeout. Check bridge logs.`);
    }
    if (profileName || headers) {
        await sendAuthCredentialsToBridge(socketPath, serverConfig.url || serverConfig.command || '', profileName, headers);
    }
    logger.debug(`Bridge started successfully for session: ${sessionName}`);
    return { pid };
}
export async function stopBridge(sessionName) {
    logger.debug(`Stopping bridge for: ${sessionName}`);
    const session = await getSession(sessionName);
    if (!session) {
        throw new ClientError(`Session not found: ${sessionName}`);
    }
    if (session.pid && isProcessAlive(session.pid)) {
        try {
            logger.debug(`Killing bridge process: ${session.pid}`);
            process.kill(session.pid, 'SIGTERM');
            await new Promise(resolve => setTimeout(resolve, 1000));
            if (isProcessAlive(session.pid)) {
                logger.debug('Bridge did not exit gracefully, sending SIGKILL');
                process.kill(session.pid, 'SIGKILL');
            }
        }
        catch (error) {
            logger.warn('Error killing bridge process:', error);
        }
        logger.debug(`Bridge stopped for ${sessionName}`);
    }
}
export async function restartBridge(sessionName) {
    logger.debug(`Trying to restart bridge for ${sessionName}...`);
    const session = await getSession(sessionName);
    if (!session) {
        throw new ClientError(`Session not found: ${sessionName}`);
    }
    try {
        await stopBridge(sessionName);
    }
    catch {
    }
    const serverConfig = { ...session.server };
    delete serverConfig.headers;
    let headers;
    const expectedHeaderKeys = session.server.headers ? Object.keys(session.server.headers) : [];
    if (expectedHeaderKeys.length > 0) {
        headers = await readKeychainSessionHeaders(sessionName);
        const retrievedHeaderKeys = new Set(Object.keys(headers || {}));
        const missingKeys = expectedHeaderKeys.filter(key => !retrievedHeaderKeys.has(key));
        if (missingKeys.length > 0) {
            throw new ClientError(`Missing HTTP header(s) in keychain for session ${sessionName}: ${missingKeys.join(', ')}. ` +
                `The session may need to be recreated with "mcpc ${sessionName} close" followed by a new connect.`);
        }
        logger.debug(`Retrieved ${expectedHeaderKeys.length} headers from keychain for failover`);
    }
    const bridgeOptions = {
        sessionName,
        serverConfig: serverConfig,
    };
    if (headers) {
        bridgeOptions.headers = headers;
    }
    if (session.profileName) {
        bridgeOptions.profileName = session.profileName;
    }
    if (session.proxy) {
        bridgeOptions.proxyConfig = session.proxy;
    }
    if (session.mcpSessionId) {
        bridgeOptions.mcpSessionId = session.mcpSessionId;
        logger.debug(`Using saved MCP session ID for resumption: ${session.mcpSessionId}`);
    }
    const { pid } = await startBridge(bridgeOptions);
    await updateSession(sessionName, { pid });
    logger.debug(`Bridge restarted for ${sessionName} with PID: ${pid}`);
    return { pid };
}
async function sendAuthCredentialsToBridge(socketPath, serverUrl, profileName, headers) {
    const credentials = {
        serverUrl,
        profileName: profileName || 'dummy',
    };
    if (profileName) {
        logger.debug(`Looking up auth profile ${profileName} for ${serverUrl}`);
        const profile = await getAuthProfile(serverUrl, profileName);
        if (profile) {
            const tokens = await readKeychainOAuthTokenInfo(profile.serverUrl, profileName);
            if (tokens?.refreshToken) {
                credentials.refreshToken = tokens.refreshToken;
                credentials.serverUrl = profile.serverUrl;
                logger.debug(`Found OAuth refresh token for profile ${profileName}`);
            }
            const clientInfo = await readKeychainOAuthClientInfo(profile.serverUrl, profileName);
            if (clientInfo?.clientId) {
                credentials.clientId = clientInfo.clientId;
                logger.debug(`Found OAuth client ID for profile ${profileName}`);
            }
        }
    }
    if (headers) {
        credentials.headers = headers;
        logger.debug(`Including ${Object.keys(headers).length} headers in credentials`);
    }
    logger.debug('Sending auth credentials to bridge' +
        (credentials.refreshToken ? ' (with refresh token)' : '') +
        (credentials.headers ? ` (with ${Object.keys(credentials.headers).length} headers)` : '') +
        (!credentials.refreshToken && !credentials.headers ? ' (minimal - no tokens or headers)' : ''));
    const client = new BridgeClient(socketPath);
    try {
        await client.connect();
        client.sendAuthCredentials(credentials);
        logger.debug('Auth credentials sent to bridge successfully');
    }
    finally {
        await client.close();
    }
}
async function checkBridgeHealth(socketPath) {
    const client = new BridgeClient(socketPath);
    try {
        await client.connect();
        await client.request('getServerDetails');
        return { healthy: true };
    }
    catch (error) {
        return { healthy: false, error: error };
    }
    finally {
        await client.close();
    }
}
export async function ensureBridgeReady(sessionName) {
    const session = await getSession(sessionName);
    if (!session) {
        throw new ClientError(`Session not found: ${sessionName}`);
    }
    if (session.status === 'expired') {
        throw new ClientError(`Session ${sessionName} has expired. ` +
            `The MCP server indicated the session is no longer valid.\n` +
            `To reconnect, run: mcpc ${sessionName} restart\n` +
            `To remove the expired session, run: mcpc ${sessionName} close`);
    }
    const socketPath = getSocketPath(sessionName);
    const processAlive = session.pid ? isProcessAlive(session.pid) : false;
    if (processAlive) {
        const result = await checkBridgeHealth(socketPath);
        if (result.healthy) {
            logger.debug(`Bridge for ${sessionName} is healthy`);
            return socketPath;
        }
        if (result.error instanceof NetworkError) {
            logger.warn(`Bridge process alive but socket not responding for ${sessionName}`);
        }
        else if (result.error) {
            const errorMessage = result.error.message || '';
            if (isAuthenticationError(errorMessage)) {
                const target = session.server.url || session.server.command || sessionName;
                throw createServerAuthError(target, { sessionName, originalError: result.error });
            }
            throw new ClientError(`Bridge for ${sessionName} failed to connect to MCP server: ${result.error.message}`);
        }
    }
    else {
        logger.debug(`Bridge process not alive for ${sessionName}, will try to restart it`);
    }
    await restartBridge(sessionName);
    const result = await checkBridgeHealth(socketPath);
    if (result.healthy) {
        logger.debug(`Bridge for ${sessionName} passed health check`);
        return socketPath;
    }
    const errorMsg = result.error?.message || 'unknown error';
    if (isAuthenticationError(errorMsg)) {
        const target = session.server.url || session.server.command || sessionName;
        throw createServerAuthError(target, {
            sessionName,
            ...(result.error && { originalError: result.error }),
        });
    }
    const logPath = `${getLogsDir()}/bridge-${sessionName}.log`;
    throw new ClientError(`Bridge for ${sessionName} failed after restart: ${errorMsg}. ` +
        `For details, check logs at ${logPath}`);
}
//# sourceMappingURL=bridge-manager.js.map