/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import * as http from 'node:http';
import * as crypto from 'node:crypto';
import { URL } from 'node:url';
import { openBrowserSecurely } from '../utils/secure-browser-launcher.js';
import { MCPOAuthTokenStorage } from './oauth-token-storage.js';
import { getErrorMessage } from '../utils/errors.js';
import { OAuthUtils, ResourceMismatchError } from './oauth-utils.js';
import { coreEvents } from '../utils/events.js';
import { debugLogger } from '../utils/debugLogger.js';
export const OAUTH_DISPLAY_MESSAGE_EVENT = 'oauth-display-message';
const REDIRECT_PATH = '/oauth/callback';
const HTTP_OK = 200;
/**
 * Provider for handling OAuth authentication for MCP servers.
 */
export class MCPOAuthProvider {
    tokenStorage;
    constructor(tokenStorage = new MCPOAuthTokenStorage()) {
        this.tokenStorage = tokenStorage;
    }
    /**
     * Register a client dynamically with the OAuth server.
     *
     * @param registrationUrl The client registration endpoint URL
     * @param config OAuth configuration
     * @param redirectPort The port to use for the redirect URI
     * @returns The registered client information
     */
    async registerClient(registrationUrl, config, redirectPort) {
        const redirectUri = config.redirectUri || `http://localhost:${redirectPort}${REDIRECT_PATH}`;
        const registrationRequest = {
            client_name: 'Gemini CLI MCP Client',
            redirect_uris: [redirectUri],
            grant_types: ['authorization_code', 'refresh_token'],
            response_types: ['code'],
            token_endpoint_auth_method: 'none', // Public client
            scope: config.scopes?.join(' ') || '',
        };
        const response = await fetch(registrationUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(registrationRequest),
        });
        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`Client registration failed: ${response.status} ${response.statusText} - ${errorText}`);
        }
        return (await response.json());
    }
    /**
     * Discover OAuth configuration from an MCP server URL.
     *
     * @param mcpServerUrl The MCP server URL
     * @returns OAuth configuration if discovered, null otherwise
     */
    async discoverOAuthFromMCPServer(mcpServerUrl) {
        // Use the full URL with path preserved for OAuth discovery
        return OAuthUtils.discoverOAuthConfig(mcpServerUrl);
    }
    async discoverAuthServerMetadataForRegistration(authorizationUrl) {
        const authUrl = new URL(authorizationUrl);
        // Preserve path components for issuers with path-based discovery (e.g., Keycloak)
        // Extract issuer by removing the OIDC protocol-specific path suffix
        // For example: http://localhost:8888/realms/my-realm/protocol/openid-connect/auth
        //           -> http://localhost:8888/realms/my-realm
        const oidcPatterns = [
            '/protocol/openid-connect/auth',
            '/protocol/openid-connect/authorize',
            '/oauth2/authorize',
            '/oauth/authorize',
            '/authorize',
        ];
        let pathname = authUrl.pathname.replace(/\/$/, ''); // Trim trailing slash
        for (const pattern of oidcPatterns) {
            if (pathname.endsWith(pattern)) {
                pathname = pathname.slice(0, -pattern.length);
                break;
            }
        }
        const issuerCandidates = new Set();
        issuerCandidates.add(authUrl.origin);
        if (pathname) {
            issuerCandidates.add(`${authUrl.origin}${pathname}`);
            const versionSegmentPattern = /^v\d+(\.\d+)?$/i;
            const segments = pathname.split('/').filter(Boolean);
            const lastSegment = segments.at(-1);
            if (lastSegment && versionSegmentPattern.test(lastSegment)) {
                const withoutVersionPath = segments.slice(0, -1);
                if (withoutVersionPath.length) {
                    issuerCandidates.add(`${authUrl.origin}/${withoutVersionPath.join('/')}`);
                }
            }
        }
        const attemptedIssuers = Array.from(issuerCandidates);
        let selectedIssuer = attemptedIssuers[0];
        let discoveredMetadata = null;
        for (const issuer of attemptedIssuers) {
            debugLogger.debug(`   Trying issuer URL: ${issuer}`);
            const metadata = await OAuthUtils.discoverAuthorizationServerMetadata(issuer);
            if (metadata) {
                selectedIssuer = issuer;
                discoveredMetadata = metadata;
                break;
            }
        }
        if (!discoveredMetadata) {
            throw new Error(`Failed to fetch authorization server metadata for client registration (attempted issuers: ${attemptedIssuers.join(', ')})`);
        }
        debugLogger.debug(`   Selected issuer URL: ${selectedIssuer}`);
        return {
            issuerUrl: selectedIssuer,
            metadata: discoveredMetadata,
        };
    }
    /**
     * Generate PKCE parameters for OAuth flow.
     *
     * @returns PKCE parameters including code verifier, challenge, and state
     */
    generatePKCEParams() {
        // Generate code verifier (43-128 characters)
        // using 64 bytes results in ~86 characters, safely above the minimum of 43
        const codeVerifier = crypto.randomBytes(64).toString('base64url');
        // Generate code challenge using SHA256
        const codeChallenge = crypto
            .createHash('sha256')
            .update(codeVerifier)
            .digest('base64url');
        // Generate state for CSRF protection
        const state = crypto.randomBytes(16).toString('base64url');
        return { codeVerifier, codeChallenge, state };
    }
    /**
     * Start a local HTTP server to handle OAuth callback.
     * The server will listen on the specified port (or port 0 for OS assignment).
     *
     * @param expectedState The state parameter to validate
     * @returns Object containing the port (available immediately) and a promise for the auth response
     */
    startCallbackServer(expectedState, port) {
        let portResolve;
        let portReject;
        const portPromise = new Promise((resolve, reject) => {
            portResolve = resolve;
            portReject = reject;
        });
        const responsePromise = new Promise((resolve, reject) => {
            let serverPort;
            const server = http.createServer(async (req, res) => {
                try {
                    const url = new URL(req.url, `http://localhost:${serverPort}`);
                    if (url.pathname !== REDIRECT_PATH) {
                        res.writeHead(404);
                        res.end('Not found');
                        return;
                    }
                    const code = url.searchParams.get('code');
                    const state = url.searchParams.get('state');
                    const error = url.searchParams.get('error');
                    if (error) {
                        res.writeHead(HTTP_OK, { 'Content-Type': 'text/html' });
                        res.end(`
              <html>
                <body>
                  <h1>Authentication Failed</h1>
                  <p>Error: ${error.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</p>
                  <p>${(url.searchParams.get('error_description') || '').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</p>
                  <p>You can close this window.</p>
                </body>
              </html>
            `);
                        server.close();
                        reject(new Error(`OAuth error: ${error}`));
                        return;
                    }
                    if (!code || !state) {
                        res.writeHead(400);
                        res.end('Missing code or state parameter');
                        return;
                    }
                    if (state !== expectedState) {
                        res.writeHead(400);
                        res.end('Invalid state parameter');
                        server.close();
                        reject(new Error('State mismatch - possible CSRF attack'));
                        return;
                    }
                    // Send success response to browser
                    res.writeHead(HTTP_OK, { 'Content-Type': 'text/html' });
                    res.end(`
            <html>
              <body>
                <h1>Authentication Successful!</h1>
                <p>You can close this window and return to Gemini CLI.</p>
                <script>window.close();</script>
              </body>
            </html>
          `);
                    server.close();
                    resolve({ code, state });
                }
                catch (error) {
                    server.close();
                    reject(error);
                }
            });
            server.on('error', (error) => {
                portReject(error);
                reject(error);
            });
            // Determine which port to use (env var, argument, or OS-assigned)
            let listenPort = 0; // Default to OS-assigned port
            const portStr = process.env['OAUTH_CALLBACK_PORT'];
            if (portStr) {
                const envPort = parseInt(portStr, 10);
                if (isNaN(envPort) || envPort <= 0 || envPort > 65535) {
                    const error = new Error(`Invalid value for OAUTH_CALLBACK_PORT: "${portStr}"`);
                    portReject(error);
                    reject(error);
                    return;
                }
                listenPort = envPort;
            }
            else if (port !== undefined) {
                listenPort = port;
            }
            server.listen(listenPort, () => {
                const address = server.address();
                serverPort = address.port;
                debugLogger.log(`OAuth callback server listening on port ${serverPort}`);
                portResolve(serverPort); // Resolve port promise immediately
            });
            // Timeout after 5 minutes
            setTimeout(() => {
                server.close();
                reject(new Error('OAuth callback timeout'));
            }, 5 * 60 * 1000);
        });
        return { port: portPromise, response: responsePromise };
    }
    /**
     * Extract the port number from a URL string if available and valid.
     *
     * @param urlString The URL string to parse
     * @returns The port number or undefined if not found or invalid
     */
    getPortFromUrl(urlString) {
        if (!urlString) {
            return undefined;
        }
        try {
            const url = new URL(urlString);
            if (url.port) {
                const parsedPort = parseInt(url.port, 10);
                if (!isNaN(parsedPort) && parsedPort > 0 && parsedPort <= 65535) {
                    return parsedPort;
                }
            }
        }
        catch {
            // Ignore invalid URL
        }
        return undefined;
    }
    /**
     * Build the authorization URL for the OAuth flow.
  
     *
     * @param config OAuth configuration
     * @param pkceParams PKCE parameters
     * @param redirectPort The port to use for the redirect URI
     * @param mcpServerUrl The MCP server URL to use as the resource parameter
     * @returns The authorization URL
     */
    buildAuthorizationUrl(config, pkceParams, redirectPort, mcpServerUrl) {
        const redirectUri = config.redirectUri || `http://localhost:${redirectPort}${REDIRECT_PATH}`;
        const params = new URLSearchParams({
            client_id: config.clientId,
            response_type: 'code',
            redirect_uri: redirectUri,
            state: pkceParams.state,
            code_challenge: pkceParams.codeChallenge,
            code_challenge_method: 'S256',
        });
        if (config.scopes && config.scopes.length > 0) {
            params.append('scope', config.scopes.join(' '));
        }
        if (config.audiences && config.audiences.length > 0) {
            params.append('audience', config.audiences.join(' '));
        }
        // Add resource parameter for MCP OAuth spec compliance
        // Only add if we have an MCP server URL (indicates MCP OAuth flow, not standard OAuth)
        if (mcpServerUrl) {
            try {
                params.append('resource', OAuthUtils.buildResourceParameter(mcpServerUrl));
            }
            catch (error) {
                debugLogger.warn(`Could not add resource parameter: ${getErrorMessage(error)}`);
            }
        }
        const url = new URL(config.authorizationUrl);
        params.forEach((value, key) => {
            url.searchParams.append(key, value);
        });
        return url.toString();
    }
    /**
     * Exchange authorization code for tokens.
     *
     * @param config OAuth configuration
     * @param code Authorization code
     * @param codeVerifier PKCE code verifier
     * @param redirectPort The port to use for the redirect URI
     * @param mcpServerUrl The MCP server URL to use as the resource parameter
     * @returns The token response
     */
    async exchangeCodeForToken(config, code, codeVerifier, redirectPort, mcpServerUrl) {
        const redirectUri = config.redirectUri || `http://localhost:${redirectPort}${REDIRECT_PATH}`;
        const params = new URLSearchParams({
            grant_type: 'authorization_code',
            code,
            redirect_uri: redirectUri,
            code_verifier: codeVerifier,
            client_id: config.clientId,
        });
        if (config.clientSecret) {
            params.append('client_secret', config.clientSecret);
        }
        if (config.audiences && config.audiences.length > 0) {
            params.append('audience', config.audiences.join(' '));
        }
        // Add resource parameter for MCP OAuth spec compliance
        // Only add if we have an MCP server URL (indicates MCP OAuth flow, not standard OAuth)
        if (mcpServerUrl) {
            const resourceUrl = mcpServerUrl;
            try {
                params.append('resource', OAuthUtils.buildResourceParameter(resourceUrl));
            }
            catch (error) {
                debugLogger.warn(`Could not add resource parameter: ${getErrorMessage(error)}`);
            }
        }
        const response = await fetch(config.tokenUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                Accept: 'application/json, application/x-www-form-urlencoded',
            },
            body: params.toString(),
        });
        const responseText = await response.text();
        const contentType = response.headers.get('content-type') || '';
        if (!response.ok) {
            // Try to parse error from form-urlencoded response
            let errorMessage = null;
            try {
                const errorParams = new URLSearchParams(responseText);
                const error = errorParams.get('error');
                const errorDescription = errorParams.get('error_description');
                if (error) {
                    errorMessage = `Token exchange failed: ${error} - ${errorDescription || 'No description'}`;
                }
            }
            catch {
                // Fall back to raw error
            }
            throw new Error(errorMessage ||
                `Token exchange failed: ${response.status} - ${responseText}`);
        }
        // Log unexpected content types for debugging
        if (!contentType.includes('application/json') &&
            !contentType.includes('application/x-www-form-urlencoded')) {
            debugLogger.warn(`Token endpoint returned unexpected content-type: ${contentType}. ` +
                `Expected application/json or application/x-www-form-urlencoded. ` +
                `Will attempt to parse response.`);
        }
        // Try to parse as JSON first, fall back to form-urlencoded
        try {
            return JSON.parse(responseText);
        }
        catch {
            // Parse form-urlencoded response
            const tokenParams = new URLSearchParams(responseText);
            const accessToken = tokenParams.get('access_token');
            const tokenType = tokenParams.get('token_type') || 'Bearer';
            const expiresIn = tokenParams.get('expires_in');
            const refreshToken = tokenParams.get('refresh_token');
            const scope = tokenParams.get('scope');
            if (!accessToken) {
                // Check for error in response
                const error = tokenParams.get('error');
                const errorDescription = tokenParams.get('error_description');
                throw new Error(`Token exchange failed: ${error || 'no_access_token'} - ${errorDescription || responseText}`);
            }
            return {
                access_token: accessToken,
                token_type: tokenType,
                expires_in: expiresIn ? parseInt(expiresIn, 10) : undefined,
                refresh_token: refreshToken || undefined,
                scope: scope || undefined,
            };
        }
    }
    /**
     * Refresh an access token using a refresh token.
     *
     * @param config OAuth configuration
     * @param refreshToken The refresh token
     * @param tokenUrl The token endpoint URL
     * @param mcpServerUrl The MCP server URL to use as the resource parameter
     * @returns The new token response
     */
    async refreshAccessToken(config, refreshToken, tokenUrl, mcpServerUrl) {
        const params = new URLSearchParams({
            grant_type: 'refresh_token',
            refresh_token: refreshToken,
            client_id: config.clientId,
        });
        if (config.clientSecret) {
            params.append('client_secret', config.clientSecret);
        }
        if (config.scopes && config.scopes.length > 0) {
            params.append('scope', config.scopes.join(' '));
        }
        if (config.audiences && config.audiences.length > 0) {
            params.append('audience', config.audiences.join(' '));
        }
        // Add resource parameter for MCP OAuth spec compliance
        // Only add if we have an MCP server URL (indicates MCP OAuth flow, not standard OAuth)
        if (mcpServerUrl) {
            try {
                params.append('resource', OAuthUtils.buildResourceParameter(mcpServerUrl));
            }
            catch (error) {
                debugLogger.warn(`Could not add resource parameter: ${getErrorMessage(error)}`);
            }
        }
        const response = await fetch(tokenUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                Accept: 'application/json, application/x-www-form-urlencoded',
            },
            body: params.toString(),
        });
        const responseText = await response.text();
        const contentType = response.headers.get('content-type') || '';
        if (!response.ok) {
            // Try to parse error from form-urlencoded response
            let errorMessage = null;
            try {
                const errorParams = new URLSearchParams(responseText);
                const error = errorParams.get('error');
                const errorDescription = errorParams.get('error_description');
                if (error) {
                    errorMessage = `Token refresh failed: ${error} - ${errorDescription || 'No description'}`;
                }
            }
            catch {
                // Fall back to raw error
            }
            throw new Error(errorMessage ||
                `Token refresh failed: ${response.status} - ${responseText}`);
        }
        // Log unexpected content types for debugging
        if (!contentType.includes('application/json') &&
            !contentType.includes('application/x-www-form-urlencoded')) {
            debugLogger.warn(`Token refresh endpoint returned unexpected content-type: ${contentType}. ` +
                `Expected application/json or application/x-www-form-urlencoded. ` +
                `Will attempt to parse response.`);
        }
        // Try to parse as JSON first, fall back to form-urlencoded
        try {
            return JSON.parse(responseText);
        }
        catch {
            // Parse form-urlencoded response
            const tokenParams = new URLSearchParams(responseText);
            const accessToken = tokenParams.get('access_token');
            const tokenType = tokenParams.get('token_type') || 'Bearer';
            const expiresIn = tokenParams.get('expires_in');
            const refreshToken = tokenParams.get('refresh_token');
            const scope = tokenParams.get('scope');
            if (!accessToken) {
                // Check for error in response
                const error = tokenParams.get('error');
                const errorDescription = tokenParams.get('error_description');
                throw new Error(`Token refresh failed: ${error || 'unknown_error'} - ${errorDescription || responseText}`);
            }
            return {
                access_token: accessToken,
                token_type: tokenType,
                expires_in: expiresIn ? parseInt(expiresIn, 10) : undefined,
                refresh_token: refreshToken || undefined,
                scope: scope || undefined,
            };
        }
    }
    /**
     * Perform the full OAuth authorization code flow with PKCE.
     *
     * @param serverName The name of the MCP server
     * @param config OAuth configuration
     * @param mcpServerUrl Optional MCP server URL for OAuth discovery
     * @param messageHandler Optional handler for displaying user-facing messages
     * @returns The obtained OAuth token
     */
    async authenticate(serverName, config, mcpServerUrl, events) {
        // Helper function to display messages through handler or fallback to console.log
        const displayMessage = (message) => {
            if (events) {
                events.emit(OAUTH_DISPLAY_MESSAGE_EVENT, message);
            }
            else {
                debugLogger.log(message);
            }
        };
        // If no authorization URL is provided, try to discover OAuth configuration
        if (!config.authorizationUrl && mcpServerUrl) {
            debugLogger.debug(`Starting OAuth for MCP server "${serverName}"…
✓ No authorization URL; using OAuth discovery`);
            // First check if the server requires authentication via WWW-Authenticate header
            try {
                const headers = OAuthUtils.isSSEEndpoint(mcpServerUrl)
                    ? { Accept: 'text/event-stream' }
                    : { Accept: 'application/json' };
                const response = await fetch(mcpServerUrl, {
                    method: 'HEAD',
                    headers,
                });
                if (response.status === 401 || response.status === 307) {
                    const wwwAuthenticate = response.headers.get('www-authenticate');
                    if (wwwAuthenticate) {
                        const discoveredConfig = await OAuthUtils.discoverOAuthFromWWWAuthenticate(wwwAuthenticate, mcpServerUrl);
                        if (discoveredConfig) {
                            // Merge discovered config with existing config, preserving clientId and clientSecret
                            config = {
                                ...config,
                                authorizationUrl: discoveredConfig.authorizationUrl,
                                tokenUrl: discoveredConfig.tokenUrl,
                                scopes: config.scopes || discoveredConfig.scopes || [],
                                // Preserve existing client credentials
                                clientId: config.clientId,
                                clientSecret: config.clientSecret,
                            };
                        }
                    }
                }
            }
            catch (error) {
                // Re-throw security validation errors
                if (error instanceof ResourceMismatchError) {
                    throw error;
                }
                debugLogger.debug(`Failed to check endpoint for authentication requirements: ${getErrorMessage(error)}`);
            }
            // If we still don't have OAuth config, try the standard discovery
            if (!config.authorizationUrl) {
                const discoveredConfig = await this.discoverOAuthFromMCPServer(mcpServerUrl);
                if (discoveredConfig) {
                    // Merge discovered config with existing config, preserving clientId and clientSecret
                    config = {
                        ...config,
                        authorizationUrl: discoveredConfig.authorizationUrl,
                        tokenUrl: discoveredConfig.tokenUrl,
                        scopes: config.scopes || discoveredConfig.scopes || [],
                        registrationUrl: discoveredConfig.registrationUrl,
                        // Preserve existing client credentials
                        clientId: config.clientId,
                        clientSecret: config.clientSecret,
                    };
                }
                else {
                    throw new Error('Failed to discover OAuth configuration from MCP server');
                }
            }
        }
        // Generate PKCE parameters
        const pkceParams = this.generatePKCEParams();
        // Determine preferred port from redirectUri if available
        const preferredPort = this.getPortFromUrl(config.redirectUri);
        // Start callback server first to allocate port
        // This ensures we only create one server and eliminates race conditions
        const callbackServer = this.startCallbackServer(pkceParams.state, preferredPort);
        // Wait for server to start and get the allocated port
        // We need this port for client registration and auth URL building
        const redirectPort = await callbackServer.port;
        debugLogger.debug(`Callback server listening on port ${redirectPort}`);
        // If no client ID is provided, try dynamic client registration
        if (!config.clientId) {
            let registrationUrl = config.registrationUrl;
            // If no registration URL was previously discovered, try to discover it
            if (!registrationUrl) {
                // Extract server URL from authorization URL
                if (!config.authorizationUrl) {
                    throw new Error('Cannot perform dynamic registration without authorization URL');
                }
                debugLogger.debug('→ Attempting dynamic client registration...');
                const { metadata: authServerMetadata } = await this.discoverAuthServerMetadataForRegistration(config.authorizationUrl);
                registrationUrl = authServerMetadata.registration_endpoint;
            }
            // Register client if registration endpoint is available
            if (registrationUrl) {
                const clientRegistration = await this.registerClient(registrationUrl, config, redirectPort);
                config.clientId = clientRegistration.client_id;
                if (clientRegistration.client_secret) {
                    config.clientSecret = clientRegistration.client_secret;
                }
                debugLogger.debug('✓ Dynamic client registration successful');
            }
            else {
                throw new Error('No client ID provided and dynamic registration not supported');
            }
        }
        // Validate configuration
        if (!config.clientId || !config.authorizationUrl || !config.tokenUrl) {
            throw new Error('Missing required OAuth configuration after discovery and registration');
        }
        // Build authorization URL
        const authUrl = this.buildAuthorizationUrl(config, pkceParams, redirectPort, mcpServerUrl);
        displayMessage(`→ Opening your browser for OAuth sign-in...

If the browser does not open, copy and paste this URL into your browser:
${authUrl}

💡 TIP: Triple-click to select the entire URL, then copy and paste it into your browser.
⚠️  Make sure to copy the COMPLETE URL - it may wrap across multiple lines.`);
        // Open browser securely (callback server is already running)
        try {
            await openBrowserSecurely(authUrl);
        }
        catch (error) {
            debugLogger.warn('Failed to open browser automatically:', getErrorMessage(error));
        }
        // Wait for callback
        const { code } = await callbackServer.response;
        debugLogger.debug('✓ Authorization code received, exchanging for tokens...');
        // Exchange code for tokens
        const tokenResponse = await this.exchangeCodeForToken(config, code, pkceParams.codeVerifier, redirectPort, mcpServerUrl);
        // Convert to our token format
        if (!tokenResponse.access_token) {
            throw new Error('No access token received from token endpoint');
        }
        const token = {
            accessToken: tokenResponse.access_token,
            tokenType: tokenResponse.token_type || 'Bearer',
            refreshToken: tokenResponse.refresh_token,
            scope: tokenResponse.scope,
        };
        if (tokenResponse.expires_in) {
            token.expiresAt = Date.now() + tokenResponse.expires_in * 1000;
        }
        // Save token
        try {
            await this.tokenStorage.saveToken(serverName, token, config.clientId, config.tokenUrl, mcpServerUrl);
            debugLogger.debug('✓ Authentication successful! Token saved.');
            // Verify token was saved
            const savedToken = await this.tokenStorage.getCredentials(serverName);
            if (savedToken && savedToken.token && savedToken.token.accessToken) {
                // Avoid leaking token material; log a short SHA-256 fingerprint instead.
                const tokenFingerprint = crypto
                    .createHash('sha256')
                    .update(savedToken.token.accessToken)
                    .digest('hex')
                    .slice(0, 8);
                debugLogger.debug(`✓ Token verification successful (fingerprint: ${tokenFingerprint})`);
            }
            else {
                debugLogger.warn('Token verification failed: token not found or invalid after save');
            }
        }
        catch (saveError) {
            debugLogger.error('Failed to save auth token.', saveError);
            throw saveError;
        }
        return token;
    }
    /**
     * Get a valid access token for an MCP server, refreshing if necessary.
     *
     * @param serverName The name of the MCP server
     * @param config OAuth configuration
     * @returns A valid access token or null if not authenticated
     */
    async getValidToken(serverName, config) {
        debugLogger.debug(`Getting valid token for server: ${serverName}`);
        const credentials = await this.tokenStorage.getCredentials(serverName);
        if (!credentials) {
            debugLogger.debug(`No credentials found for server: ${serverName}`);
            return null;
        }
        const { token } = credentials;
        debugLogger.debug(`Found token for server: ${serverName}, expired: ${this.tokenStorage.isTokenExpired(token)}`);
        // Check if token is expired
        if (!this.tokenStorage.isTokenExpired(token)) {
            debugLogger.debug(`Returning valid token for server: ${serverName}`);
            return token.accessToken;
        }
        // Try to refresh if we have a refresh token
        if (token.refreshToken && config.clientId && credentials.tokenUrl) {
            try {
                debugLogger.log(`Refreshing expired token for MCP server: ${serverName}`);
                const newTokenResponse = await this.refreshAccessToken(config, token.refreshToken, credentials.tokenUrl, credentials.mcpServerUrl);
                // Update stored token
                const newToken = {
                    accessToken: newTokenResponse.access_token,
                    tokenType: newTokenResponse.token_type,
                    refreshToken: newTokenResponse.refresh_token || token.refreshToken,
                    scope: newTokenResponse.scope || token.scope,
                };
                if (newTokenResponse.expires_in) {
                    newToken.expiresAt = Date.now() + newTokenResponse.expires_in * 1000;
                }
                await this.tokenStorage.saveToken(serverName, newToken, config.clientId, credentials.tokenUrl, credentials.mcpServerUrl);
                return newToken.accessToken;
            }
            catch (error) {
                coreEvents.emitFeedback('error', 'Failed to refresh auth token.', error);
                // Remove invalid token
                await this.tokenStorage.deleteCredentials(serverName);
            }
        }
        return null;
    }
}
//# sourceMappingURL=oauth-provider.js.map