import { createServer } from 'http';
import { URL } from 'url';
import { auth as sdkAuth } from '@modelcontextprotocol/sdk/client/auth.js';
import { OAuthProvider } from './oauth-provider.js';
import { normalizeServerUrl } from '../utils.js';
import { ClientError } from '../errors.js';
import { createLogger } from '../logger.js';
import { removeKeychainOAuthClientInfo } from './keychain.js';
const logger = createLogger('oauth-flow');
const ESCAPE_KEY = '\x1b';
const CTRL_C = '\x03';
const ENTER_CR = '\r';
const ENTER_LF = '\n';
function setupKeyListener(onKey) {
    let cleanup = () => { };
    let cleaned = false;
    const promise = new Promise((resolve, reject) => {
        if (!process.stdin.isTTY) {
            return;
        }
        const onData = (key) => {
            const result = onKey(key.toString());
            if (result.done) {
                cleanup();
                if ('error' in result) {
                    reject(result.error);
                }
                else {
                    resolve(result.value);
                }
            }
        };
        process.stdin.setRawMode(true);
        process.stdin.resume();
        process.stdin.on('data', onData);
        cleanup = () => {
            if (cleaned)
                return;
            cleaned = true;
            process.stdin.off('data', onData);
            process.stdin.setRawMode(false);
            process.stdin.pause();
        };
    });
    return { promise, cleanup };
}
function waitForEscapeKey() {
    const { promise, cleanup } = setupKeyListener((char) => {
        if (char === ESCAPE_KEY || char === CTRL_C) {
            return { done: true, error: new ClientError('Authentication cancelled by user') };
        }
        return { done: false };
    });
    return { promise, cleanup };
}
async function waitForEnterKey(prompt) {
    if (!process.stdin.isTTY) {
        return true;
    }
    process.stdout.write(prompt);
    const { promise } = setupKeyListener((char) => {
        console.log('');
        if (char === ENTER_CR || char === ENTER_LF) {
            return { done: true, value: true };
        }
        return { done: true, value: false };
    });
    return promise;
}
function startCallbackServer(port) {
    let resolveCode;
    let rejectCode;
    const codePromise = new Promise((resolve, reject) => {
        resolveCode = resolve;
        rejectCode = reject;
    });
    const sockets = new Set();
    const server = createServer((req, res) => {
        const url = new URL(req.url || '/', `http://localhost:${port}`);
        if (url.pathname === '/callback') {
            const code = url.searchParams.get('code');
            const error = url.searchParams.get('error');
            const errorDescription = url.searchParams.get('error_description');
            const state = url.searchParams.get('state') || undefined;
            if (error) {
                const message = errorDescription || error;
                res.writeHead(400, { 'Content-Type': 'text/html' });
                res.end(`
          <html>
            <head><title>Authentication failed</title></head>
            <body>
              <h1>Authentication failed</h1>
              <p>Error: ${message}</p>
              <p>You can close this window.</p>
            </body>
          </html>
        `);
                rejectCode(new ClientError(`OAuth error: ${message}`));
                return;
            }
            if (!code) {
                res.writeHead(400, { 'Content-Type': 'text/html' });
                res.end(`
          <html>
            <head><title>Authentication failed</title></head>
            <body>
              <h1>Authentication failed</h1>
              <p>No authorization code received</p>
              <p>You can close this window.</p>
            </body>
          </html>
        `);
                rejectCode(new ClientError('No authorization code in callback'));
                return;
            }
            res.writeHead(200, { 'Content-Type': 'text/html' });
            res.end(`
        <html>
          <head><title>Authentication successful</title></head>
          <body>
            <h1>Authentication successful!</h1>
            <p>You can close this window and return to the terminal.</p>
          </body>
        </html>
      `);
            const result = { code };
            if (state !== undefined) {
                result.state = state;
            }
            resolveCode(result);
        }
        else {
            res.writeHead(404);
            res.end('Not found');
        }
    });
    server.on('connection', (socket) => {
        sockets.add(socket);
        socket.on('close', () => {
            sockets.delete(socket);
        });
    });
    const destroyConnections = () => {
        for (const socket of sockets) {
            socket.destroy();
        }
        sockets.clear();
    };
    return { server, codePromise, destroyConnections };
}
async function findAvailablePort(startPort = 8000) {
    for (let port = startPort; port < startPort + 100; port++) {
        try {
            await new Promise((resolve, reject) => {
                const testServer = createServer();
                testServer.once('error', reject);
                testServer.once('listening', () => {
                    testServer.close(() => resolve());
                });
                testServer.listen(port, '127.0.0.1');
            });
            return port;
        }
        catch {
        }
    }
    throw new ClientError('Could not find available port for OAuth callback server');
}
async function openBrowser(url) {
    const { exec } = await import('child_process');
    const { promisify } = await import('util');
    const execAsync = promisify(exec);
    const platform = process.platform;
    let command;
    if (platform === 'darwin') {
        command = `open "${url}"`;
    }
    else if (platform === 'win32') {
        command = `start "${url}"`;
    }
    else {
        command = `xdg-open "${url}"`;
    }
    try {
        await execAsync(command);
        logger.debug('Browser opened successfully');
    }
    catch (error) {
        logger.warn(`Failed to open browser: ${error.message}`);
        throw new ClientError(`Failed to open browser. Please manually navigate to: ${url}`);
    }
}
export async function performOAuthFlow(serverUrl, profileName, scope) {
    logger.debug(`Starting OAuth flow for ${serverUrl} (profile: ${profileName})`);
    const normalizedServerUrl = normalizeServerUrl(serverUrl);
    const parsedUrl = new URL(normalizedServerUrl);
    if (parsedUrl.protocol === 'http:' && parsedUrl.hostname !== 'localhost' && parsedUrl.hostname !== '127.0.0.1') {
        console.warn('\nWarning: OAuth over plain HTTP is insecure. Only use for local development.\n');
    }
    const port = await findAvailablePort(8000);
    const redirectUrl = `http://localhost:${port}/callback`;
    logger.debug(`Using redirect URL: ${redirectUrl}`);
    logger.debug(`Removing existing OAuth client info for ${profileName} @ ${normalizedServerUrl}`);
    await removeKeychainOAuthClientInfo(normalizedServerUrl, profileName);
    const provider = new OAuthProvider({
        serverUrl: normalizedServerUrl,
        profileName,
        redirectUrl,
        forceReauth: true,
    });
    const { server, codePromise, destroyConnections } = startCallbackServer(port);
    try {
        await new Promise((resolve, reject) => {
            server.once('error', reject);
            server.listen(port, '127.0.0.1', () => {
                logger.debug(`Callback server listening on port ${port}`);
                resolve();
            });
        });
        const escapeHandlerRef = { current: null };
        provider.redirectToAuthorization = async (authorizationUrl) => {
            logger.debug('Opening browser for authorization...');
            console.log(`\nAuthorization URL: ${authorizationUrl.toString()}`);
            const confirmed = await waitForEnterKey('Press Enter to open browser (any other key to cancel): ');
            if (!confirmed) {
                throw new ClientError('Authentication cancelled by user');
            }
            console.log('Opening browser...');
            console.log('If the browser does not open automatically, please visit the URL above.');
            console.log('Press Esc to cancel.\n');
            escapeHandlerRef.current = waitForEscapeKey();
            try {
                await openBrowser(authorizationUrl.toString());
            }
            catch (error) {
                console.error(error.message);
            }
        };
        try {
            logger.debug('Calling SDK auth()...');
            const authOptions = {
                serverUrl: normalizedServerUrl,
            };
            if (scope !== undefined) {
                authOptions.scope = scope;
            }
            const result = await sdkAuth(provider, authOptions);
            if (result === 'REDIRECT') {
                logger.debug('Waiting for authorization code...');
                const racers = [codePromise];
                if (escapeHandlerRef.current) {
                    racers.push(escapeHandlerRef.current.promise);
                }
                const { code } = await Promise.race(racers);
                logger.debug('Exchanging authorization code for tokens...');
                const tokenOptions = {
                    serverUrl: normalizedServerUrl,
                    authorizationCode: code,
                };
                if (scope !== undefined) {
                    tokenOptions.scope = scope;
                }
                await sdkAuth(provider, tokenOptions);
            }
        }
        finally {
            escapeHandlerRef.current?.cleanup();
        }
        const profile = provider._authProfile;
        if (!profile) {
            throw new ClientError('Failed to save authentication profile');
        }
        logger.debug('OAuth flow completed successfully');
        return {
            profile,
            success: true,
        };
    }
    catch (error) {
        logger.debug(`OAuth flow failed: ${error.message}`);
        throw error;
    }
    finally {
        destroyConnections();
        server.close();
    }
}
//# sourceMappingURL=oauth-flow.js.map