#!/usr/bin/env node
import { createServer } from 'net';
import { unlink } from 'fs/promises';
import { createMcpClient } from '../core/index.js';
import { createLogger, setVerbose, initFileLogger, closeFileLogger } from '../lib/index.js';
import { fileExists, getBridgesDir, getSocketPath, ensureDir, cleanupOrphanedLogFiles, isSessionExpiredError } from '../lib/index.js';
import { ClientError, NetworkError } from '../lib/index.js';
import { loadSessions, updateSession } from '../lib/sessions.js';
import { OAuthTokenManager } from '../lib/auth/oauth-token-manager.js';
import { OAuthProvider } from '../lib/auth/oauth-provider.js';
import { storeKeychainOAuthTokenInfo, readKeychainOAuthTokenInfo } from '../lib/auth/keychain.js';
import { updateAuthProfileRefreshedAt } from '../lib/auth/profiles.js';
import { readKeychainProxyBearerToken } from '../lib/auth/keychain.js';
import packageJson from '../../package.json' with { type: 'json' };
import { ProxyServer } from './proxy-server.js';
const KEEPALIVE_INTERVAL_MS = 30_000;
const logger = createLogger('bridge');
class BridgeProcess {
    client = null;
    server = null;
    proxyServer = null;
    connections = new Set();
    options;
    socketPath;
    isShuttingDown = false;
    keepaliveInterval = null;
    tokenManager = null;
    authProvider = null;
    headers = null;
    authCredentialsReceived = null;
    authCredentialsResolver = null;
    mcpClientReady;
    mcpClientReadyResolver;
    mcpClientReadyRejecter;
    constructor(options) {
        this.options = options;
        this.socketPath = getSocketPath(options.sessionName);
        this.mcpClientReady = new Promise((resolve, reject) => {
            this.mcpClientReadyResolver = resolve;
            this.mcpClientReadyRejecter = reject;
        });
        if (options.verbose) {
            setVerbose(true);
        }
    }
    setAuthCredentials(credentials) {
        logger.info(`Received auth credentials for profile: ${credentials.profileName}`);
        logger.debug(`  serverUrl: ${credentials.serverUrl}`);
        logger.debug(`  refreshToken: ${credentials.refreshToken ? 'present' : 'MISSING'}`);
        logger.debug(`  clientId: ${credentials.clientId ? 'present' : 'MISSING'}`);
        logger.debug(`  headers: ${credentials.headers ? Object.keys(credentials.headers).length : 0}`);
        if (credentials.refreshToken && credentials.clientId) {
            this.tokenManager = new OAuthTokenManager({
                serverUrl: credentials.serverUrl,
                profileName: credentials.profileName,
                clientId: credentials.clientId,
                refreshToken: credentials.refreshToken,
                onBeforeRefresh: async () => {
                    logger.debug('Reloading tokens from keychain before refresh...');
                    const tokenInfo = await readKeychainOAuthTokenInfo(credentials.serverUrl, credentials.profileName);
                    if (!tokenInfo) {
                        logger.debug('No tokens found in keychain');
                        return undefined;
                    }
                    logger.debug('Loaded tokens from keychain');
                    const result = {
                        accessToken: tokenInfo.accessToken,
                    };
                    if (tokenInfo.refreshToken) {
                        result.refreshToken = tokenInfo.refreshToken;
                    }
                    if (tokenInfo.expiresAt !== undefined) {
                        result.accessTokenExpiresAt = tokenInfo.expiresAt;
                    }
                    return result;
                },
                onTokenRefresh: async (tokens) => {
                    logger.debug('Token refresh detected, persisting new tokens to keychain');
                    const tokenInfo = {
                        accessToken: tokens.access_token,
                        tokenType: tokens.token_type,
                    };
                    if (tokens.expires_in !== undefined) {
                        tokenInfo.expiresIn = tokens.expires_in;
                        tokenInfo.expiresAt = Math.floor(Date.now() / 1000) + tokens.expires_in;
                    }
                    if (tokens.refresh_token !== undefined) {
                        tokenInfo.refreshToken = tokens.refresh_token;
                    }
                    if (tokens.scope !== undefined) {
                        tokenInfo.scope = tokens.scope;
                    }
                    await storeKeychainOAuthTokenInfo(credentials.serverUrl, credentials.profileName, tokenInfo);
                    await updateAuthProfileRefreshedAt(credentials.serverUrl, credentials.profileName);
                    logger.debug('Profile refreshedAt timestamp updated');
                    logger.debug('New tokens persisted to keychain');
                },
            });
            logger.debug('OAuth token manager initialized');
            this.authProvider = new OAuthProvider({
                serverUrl: credentials.serverUrl,
                profileName: credentials.profileName,
                tokenManager: this.tokenManager,
                clientId: credentials.clientId,
            });
            logger.debug('OAuthProvider created for SDK transport (runtime mode)');
        }
        else if (credentials.refreshToken && !credentials.clientId) {
            logger.warn('Refresh token provided but client ID is missing - token refresh will not work');
        }
        if (credentials.headers) {
            this.headers = credentials.headers;
            logger.debug(`Stored headers "${Object.keys(this.headers).join(', ')}" in memory`);
        }
        if (this.authCredentialsResolver) {
            this.authCredentialsResolver();
            this.authCredentialsResolver = null;
        }
    }
    async updateTransportAuth() {
        const config = { ...this.options.serverConfig };
        if (!config.url) {
            return config;
        }
        try {
            if (this.tokenManager) {
                const token = await this.tokenManager.getValidAccessToken();
                config.headers = {
                    ...this.headers,
                    ...config.headers,
                    Authorization: `Bearer ${token}`,
                };
                logger.debug('Updated transport config with fresh OAuth access token');
                return config;
            }
            if (this.headers) {
                config.headers = {
                    ...config.headers,
                    ...this.headers,
                };
                logger.debug(`Updated transport config with ${Object.keys(this.headers).length} headers`);
                return config;
            }
        }
        catch (error) {
            logger.error('Failed to get access token:', error);
            await this.markSessionExpiredAndExit();
            throw error;
        }
        return config;
    }
    runOrphanedLogCleanup() {
        void (async () => {
            try {
                logger.debug('Starting cleanup of orphaned log files');
                const sessionsStorage = await loadSessions();
                const activeSessions = sessionsStorage.sessions;
                const deleted = await cleanupOrphanedLogFiles(activeSessions, {
                    skipSession: this.options.sessionName,
                });
                logger.debug(`Finished cleanup of orphaned log files (deleted: ${deleted})`);
            }
            catch (error) {
                logger.warn('Failed to clean up orphaned log files:', error);
            }
        })();
    }
    async start() {
        await initFileLogger(`bridge-${this.options.sessionName}.log`, {
            version: packageJson.version,
            command: process.argv.slice(1).join(' '),
        });
        logger.info(`Bridge process starting for session: ${this.options.sessionName}`);
        this.runOrphanedLogCleanup();
        try {
            await this.createSocketServer();
            if (this.options.profileName) {
                logger.debug(`Waiting for auth credentials (profile: ${this.options.profileName})...`);
                this.authCredentialsReceived = new Promise((resolve) => {
                    this.authCredentialsResolver = resolve;
                });
                const timeout = new Promise((_, reject) => {
                    setTimeout(() => reject(new Error('Timeout waiting for auth credentials')), 5000);
                });
                await Promise.race([this.authCredentialsReceived, timeout]);
                logger.debug('Auth credentials received, proceeding with MCP connection');
            }
            try {
                await this.connectToMcp();
                if (this.options.proxyConfig && this.client) {
                    await this.startProxyServer();
                }
                this.mcpClientReadyResolver();
            }
            catch (error) {
                logger.error('Bridge startup failed, will stay alive briefly for CLI to receive error:', error);
                this.mcpClientReadyRejecter(error);
                if (isSessionExpiredError(error.message)) {
                    logger.warn('Session ID was rejected by server, marking session as expired');
                    try {
                        await updateSession(this.options.sessionName, { status: 'expired' });
                    }
                    catch (updateError) {
                        logger.error('Failed to mark session as expired:', updateError);
                    }
                }
                this.setupSignalHandlers();
                setTimeout(() => {
                    logger.info('Shutting down after startup failure');
                    this.shutdown().catch(() => process.exit(1));
                }, 10_000);
                return;
            }
            this.startKeepalive();
            this.setupSignalHandlers();
            logger.info('Bridge process started successfully');
        }
        catch (error) {
            logger.error('Failed to start bridge:', error);
            await this.cleanup();
            throw error;
        }
    }
    broadcastNotification(method, params) {
        const notification = {
            type: 'notification',
            notification: {
                method: method,
                params,
            },
        };
        const data = JSON.stringify(notification) + '\n';
        for (const socket of this.connections) {
            try {
                socket.write(data);
            }
            catch (error) {
                logger.error('Failed to send notification to client:', error);
            }
        }
    }
    async updateNotificationTimestamp(type) {
        const now = new Date().toISOString();
        await updateSession(this.options.sessionName, {
            notifications: {
                [type]: { listChangedAt: now },
            },
        });
        logger.debug(`Updated ${type} listChangedAt timestamp`);
    }
    async connectToMcp() {
        logger.debug('Connecting to MCP server...');
        logger.debug(`  authProvider: ${this.authProvider ? 'present' : 'NOT SET'}`);
        logger.debug(`  tokenManager: ${this.tokenManager ? 'present' : 'NOT SET'}`);
        logger.debug(`  headers: ${this.headers ? Object.keys(this.headers).join(', ') : 'none'}`);
        let serverConfig;
        if (this.authProvider) {
            serverConfig = { ...this.options.serverConfig };
            if (this.headers) {
                serverConfig.headers = { ...serverConfig.headers, ...this.headers };
                logger.debug(`Added non-OAuth headers "${Object.keys(this.headers).join(', ')}" to transport`);
            }
        }
        else {
            logger.debug('No authProvider - using updateTransportAuth for headers');
            serverConfig = await this.updateTransportAuth();
        }
        logger.debug('Building MCP client config...');
        logger.debug(`  this.authProvider is set: ${!!this.authProvider}`);
        if (this.authProvider) {
            logger.debug(`  authProvider type: ${this.authProvider.constructor.name}`);
        }
        const clientConfig = {
            clientInfo: { name: 'mcpc', version: packageJson.version },
            serverConfig,
            capabilities: {
                roots: { listChanged: true },
                sampling: {},
            },
            ...(this.authProvider && { authProvider: this.authProvider }),
            ...(this.options.mcpSessionId && { mcpSessionId: this.options.mcpSessionId }),
            listChanged: {
                tools: {
                    autoRefresh: true,
                    onChanged: (error, tools) => {
                        logger.debug('Tools list changed', { error, count: tools?.length });
                        this.broadcastNotification('tools/list_changed');
                        this.updateNotificationTimestamp('tools').catch((err) => {
                            logger.warn('Failed to update tools notification timestamp:', err);
                        });
                    },
                },
                resources: {
                    autoRefresh: true,
                    onChanged: (error, resources) => {
                        logger.debug('Resources list changed', { error, count: resources?.length });
                        this.broadcastNotification('resources/list_changed');
                        this.updateNotificationTimestamp('resources').catch((err) => {
                            logger.warn('Failed to update resources notification timestamp:', err);
                        });
                    },
                },
                prompts: {
                    autoRefresh: true,
                    onChanged: (error, prompts) => {
                        logger.debug('Prompts list changed', { error, count: prompts?.length });
                        this.broadcastNotification('prompts/list_changed');
                        this.updateNotificationTimestamp('prompts').catch((err) => {
                            logger.warn('Failed to update prompts notification timestamp:', err);
                        });
                    },
                },
            },
            autoConnect: true,
            verbose: this.options.verbose || false,
        };
        logger.debug('Calling createMcpClient with authProvider:', !!clientConfig.authProvider);
        if (clientConfig.mcpSessionId) {
            logger.info(`Attempting session resumption with MCP-Session-Id: ${clientConfig.mcpSessionId}`);
        }
        this.client = await createMcpClient(clientConfig);
        logger.info('Connected to MCP server');
        logger.debug('MCP client created successfully, authProvider was:', !!clientConfig.authProvider);
        const serverDetails = await this.client.getServerDetails();
        const newMcpSessionId = this.client.getMcpSessionId();
        const sessionUpdate = {
            lastSeenAt: new Date().toISOString(),
        };
        if (serverDetails.protocolVersion) {
            sessionUpdate.protocolVersion = serverDetails.protocolVersion;
        }
        if (serverDetails.serverInfo) {
            sessionUpdate.serverInfo = serverDetails.serverInfo;
        }
        if (newMcpSessionId) {
            sessionUpdate.mcpSessionId = newMcpSessionId;
            logger.info(`MCP-Session-Id saved for resumption: ${newMcpSessionId}`);
        }
        await updateSession(this.options.sessionName, sessionUpdate);
    }
    async startProxyServer() {
        if (!this.options.proxyConfig || !this.client) {
            return;
        }
        const { host, port } = this.options.proxyConfig;
        const bearerToken = await readKeychainProxyBearerToken(this.options.sessionName);
        logger.info(`Starting proxy server on ${host}:${port}`);
        if (bearerToken) {
            logger.debug('Proxy server will require bearer token authentication');
        }
        const serverDetails = await this.client.getServerDetails();
        const instructions = serverDetails.instructions;
        const proxyOptions = {
            host,
            port,
            client: this.client,
            sessionName: this.options.sessionName,
        };
        if (bearerToken) {
            proxyOptions.bearerToken = bearerToken;
        }
        if (instructions) {
            proxyOptions.instructions = instructions;
        }
        this.proxyServer = new ProxyServer(proxyOptions);
        await this.proxyServer.start();
        logger.info(`Proxy server started at ${this.proxyServer.getAddress()}`);
    }
    startKeepalive() {
        logger.debug(`Starting keepalive ping every ${KEEPALIVE_INTERVAL_MS / 1000}s`);
        this.keepaliveInterval = setInterval(() => {
            this.sendKeepalivePing().catch((error) => {
                logger.error('Keepalive ping failed:', error);
                this.handlePossibleExpiration(error);
            });
        }, KEEPALIVE_INTERVAL_MS);
        this.keepaliveInterval.unref();
    }
    async sendKeepalivePing() {
        if (!this.client || this.isShuttingDown) {
            return;
        }
        logger.debug('Sending keepalive ping');
        await this.client.ping();
        logger.debug('Keepalive ping successful');
        await this.updateLastSeenAt();
    }
    async updateLastSeenAt() {
        try {
            await updateSession(this.options.sessionName, {
                lastSeenAt: new Date().toISOString(),
            });
        }
        catch (error) {
            logger.error('Failed to update lastSeenAt:', error);
        }
    }
    handlePossibleExpiration(error) {
        if (isSessionExpiredError(error.message)) {
            logger.warn('Session appears to be expired, marking as expired and shutting down');
            this.markSessionExpiredAndExit().catch((e) => {
                logger.error('Failed to mark session as expired:', e);
                process.exit(1);
            });
        }
    }
    async markSessionExpiredAndExit() {
        if (this.isShuttingDown) {
            return;
        }
        try {
            await updateSession(this.options.sessionName, { status: 'expired' });
            logger.info(`Session ${this.options.sessionName} marked as expired`);
        }
        catch (error) {
            logger.error('Failed to update session status:', error);
        }
        await this.shutdown();
    }
    async createSocketServer() {
        if (process.platform !== 'win32') {
            await ensureDir(getBridgesDir());
            if (await fileExists(this.socketPath)) {
                logger.debug(`Removing existing socket: ${this.socketPath}`);
                await unlink(this.socketPath);
            }
        }
        const server = createServer((socket) => this.handleConnection(socket));
        this.server = server;
        await new Promise((resolve, reject) => {
            server.listen(this.socketPath, () => {
                logger.info(`Socket server listening: ${this.socketPath}`);
                resolve();
            });
            server.on('error', (error) => {
                logger.error('Socket server error:', error);
                reject(error);
            });
        });
    }
    handleConnection(socket) {
        logger.debug('New client connected');
        this.connections.add(socket);
        let buffer = '';
        socket.on('data', (data) => {
            buffer += data.toString();
            let newlineIndex;
            while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
                const line = buffer.slice(0, newlineIndex);
                buffer = buffer.slice(newlineIndex + 1);
                if (line.trim()) {
                    this.handleMessage(socket, line).catch((error) => {
                        logger.error('Error handling message:', error);
                    });
                }
            }
        });
        socket.on('end', () => {
            logger.debug('Client disconnected');
            this.connections.delete(socket);
        });
        socket.on('error', (error) => {
            logger.error('Socket error:', error);
            this.connections.delete(socket);
        });
    }
    async handleMessage(socket, data) {
        let messageId;
        try {
            const message = JSON.parse(data);
            messageId = message.id;
            logger.debug('Received message:', { type: message.type, method: message.method });
            switch (message.type) {
                case 'request':
                    await this.handleMcpRequest(socket, message);
                    break;
                case 'shutdown':
                    logger.info('Received shutdown request');
                    if (message.id) {
                        this.sendResponse(socket, { type: 'response', id: message.id });
                    }
                    await this.shutdown();
                    break;
                case 'set-auth-credentials':
                    if (message.authCredentials) {
                        this.setAuthCredentials(message.authCredentials);
                        if (message.id) {
                            this.sendResponse(socket, {
                                type: 'response',
                                id: message.id,
                                result: { success: true },
                            });
                        }
                    }
                    else {
                        throw new ClientError('Missing authCredentials in set-auth-credentials message');
                    }
                    break;
                default:
                    throw new ClientError(`Unknown message type: ${message.type}`);
            }
        }
        catch (error) {
            logger.error('Failed to handle message:', error);
            this.sendError(socket, error, messageId);
        }
    }
    async handleMcpRequest(socket, message) {
        logger.debug(`>>> handleMcpRequest: ${message.method} <<<`);
        await this.mcpClientReady;
        if (!this.client) {
            throw new NetworkError('MCP client not connected');
        }
        if (!message.method) {
            throw new ClientError('Missing method in request');
        }
        logger.debug(`  authProvider is set: ${!!this.authProvider}`);
        if (this.tokenManager) {
            const expiresIn = this.tokenManager.getSecondsUntilExpiry();
            logger.debug(`  token expires in: ${expiresIn} seconds`);
        }
        try {
            let result;
            switch (message.method) {
                case 'ping':
                    result = await this.client.ping();
                    break;
                case 'listTools': {
                    const cursor = message.params;
                    result = await this.client.listTools(cursor);
                    break;
                }
                case 'callTool': {
                    const params = message.params;
                    result = await this.client.callTool(params.name, params.arguments);
                    break;
                }
                case 'listResources': {
                    const cursor = message.params;
                    result = await this.client.listResources(cursor);
                    break;
                }
                case 'readResource': {
                    const params = message.params;
                    result = await this.client.readResource(params.uri);
                    break;
                }
                case 'listResourceTemplates': {
                    const cursor = message.params;
                    result = await this.client.listResourceTemplates(cursor);
                    break;
                }
                case 'subscribeResource': {
                    const params = message.params;
                    result = await this.client.subscribeResource(params.uri);
                    break;
                }
                case 'unsubscribeResource': {
                    const params = message.params;
                    result = await this.client.unsubscribeResource(params.uri);
                    break;
                }
                case 'listPrompts': {
                    const cursor = message.params;
                    result = await this.client.listPrompts(cursor);
                    break;
                }
                case 'getPrompt': {
                    const params = message.params;
                    result = await this.client.getPrompt(params.name, params.arguments);
                    break;
                }
                case 'setLoggingLevel': {
                    const params = message.params;
                    result = await this.client.setLoggingLevel(params);
                    break;
                }
                case 'getServerDetails':
                    result = await this.client.getServerDetails();
                    break;
                default:
                    throw new ClientError(`Unknown MCP method: ${message.method}`);
            }
            if (message.id) {
                this.sendResponse(socket, {
                    type: 'response',
                    id: message.id,
                    result,
                });
            }
        }
        catch (error) {
            logger.error('Failed to forward MCP request to server:', error);
            this.sendError(socket, error, message.id);
            this.handlePossibleExpiration(error);
        }
    }
    sendResponse(socket, message) {
        const data = JSON.stringify(message) + '\n';
        socket.write(data);
    }
    sendError(socket, error, id) {
        const message = {
            type: 'response',
            error: {
                code: error instanceof ClientError ? 1 : error instanceof NetworkError ? 3 : 2,
                message: error.message,
            },
        };
        if (id) {
            message.id = id;
        }
        this.sendResponse(socket, message);
    }
    setupSignalHandlers() {
        const handleSignal = (signal) => {
            logger.info(`Received ${signal}, shutting down...`);
            this.shutdown().catch((error) => {
                logger.error('Error during shutdown:', error);
                process.exit(1);
            });
        };
        process.on('SIGTERM', () => handleSignal('SIGTERM'));
        process.on('SIGINT', () => handleSignal('SIGINT'));
    }
    async shutdown() {
        if (this.isShuttingDown) {
            return;
        }
        this.isShuttingDown = true;
        logger.info('Shutting down bridge...');
        await this.cleanup();
        process.exit(0);
    }
    async cleanup() {
        if (this.keepaliveInterval) {
            clearInterval(this.keepaliveInterval);
            this.keepaliveInterval = null;
            logger.debug('Keepalive interval stopped');
        }
        if (this.proxyServer) {
            try {
                await this.proxyServer.stop();
                this.proxyServer = null;
                logger.debug('Proxy server stopped');
            }
            catch (error) {
                logger.warn('Failed to stop proxy server:', error);
            }
        }
        for (const socket of this.connections) {
            socket.end();
        }
        this.connections.clear();
        if (this.server) {
            const server = this.server;
            await new Promise((resolve) => {
                server.close(() => {
                    logger.debug('Socket server closed');
                    resolve();
                });
            });
            if (process.platform !== 'win32') {
                try {
                    if (await fileExists(this.socketPath)) {
                        await unlink(this.socketPath);
                        logger.debug('Socket file removed');
                    }
                }
                catch (error) {
                    logger.warn('Failed to remove socket file:', error);
                }
            }
        }
        if (this.client) {
            try {
                await this.client.close();
                logger.debug('MCP client closed');
            }
            catch (error) {
                logger.warn('Failed to close MCP client:', error);
            }
        }
        try {
            await closeFileLogger();
            logger.debug('File logger closed');
        }
        catch (error) {
            logger.warn('Failed to close file logger:', error);
        }
    }
}
async function main() {
    const args = process.argv.slice(2);
    if (args.length < 2) {
        console.error('Usage: mcpc-bridge <sessionName> <transportConfigJson> [--verbose] [--profile <name>] [--proxy-host <host>] [--proxy-port <port>] [--mcp-session-id <id>]');
        process.exit(1);
    }
    const sessionName = args[0];
    const transportConfigJson = args[1];
    const verbose = args.includes('--verbose');
    let profileName;
    const profileIndex = args.indexOf('--profile');
    if (profileIndex !== -1 && args[profileIndex + 1]) {
        profileName = args[profileIndex + 1];
    }
    let proxyConfig;
    const proxyHostIndex = args.indexOf('--proxy-host');
    const proxyPortIndex = args.indexOf('--proxy-port');
    if (proxyHostIndex !== -1 && proxyPortIndex !== -1) {
        const host = args[proxyHostIndex + 1];
        const port = parseInt(args[proxyPortIndex + 1] || '', 10);
        if (host && !isNaN(port)) {
            proxyConfig = { host, port };
        }
    }
    let mcpSessionId;
    const mcpSessionIdIndex = args.indexOf('--mcp-session-id');
    if (mcpSessionIdIndex !== -1 && args[mcpSessionIdIndex + 1]) {
        mcpSessionId = args[mcpSessionIdIndex + 1];
    }
    try {
        const bridgeOptions = {
            sessionName,
            serverConfig: JSON.parse(transportConfigJson),
            verbose,
        };
        if (profileName) {
            bridgeOptions.profileName = profileName;
        }
        if (proxyConfig) {
            bridgeOptions.proxyConfig = proxyConfig;
        }
        if (mcpSessionId) {
            bridgeOptions.mcpSessionId = mcpSessionId;
        }
        const bridge = new BridgeProcess(bridgeOptions);
        await bridge.start();
    }
    catch (error) {
        console.error('Bridge failed to start:', error);
        process.exit(1);
    }
}
main().catch((error) => {
    console.error('Fatal error:', error);
    process.exit(1);
});
//# sourceMappingURL=index.js.map