/**
 * @license
 * Copyright 2025 Vybestack LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { SettingScope } from '../config/settings.js';
import { getSettingsService } from '@vybestack/llxprt-code-core';
function isAuthOnlyEnabled(value) {
    if (typeof value === 'boolean') {
        return value;
    }
    if (typeof value === 'string') {
        const normalized = value.trim().toLowerCase();
        if (normalized === 'true') {
            return true;
        }
        if (normalized === 'false') {
            return false;
        }
    }
    return false;
}
/**
 * OAuth Manager coordinates multiple OAuth providers
 * Provides unified interface for authentication across providers
 */
export class OAuthManager {
    providers;
    tokenStore;
    settings;
    // In-memory OAuth enablement state for when settings aren't available
    inMemoryOAuthState;
    constructor(tokenStore, settings) {
        this.providers = new Map();
        this.tokenStore = tokenStore;
        this.settings = settings;
        this.inMemoryOAuthState = new Map();
    }
    /**
     * Register an OAuth provider with the manager
     * @param provider - The OAuth provider to register
     */
    registerProvider(provider) {
        if (!provider) {
            throw new Error('Provider cannot be null or undefined');
        }
        if (!provider.name || typeof provider.name !== 'string') {
            throw new Error('Provider must have a valid name');
        }
        // Validate provider has required methods
        if (typeof provider.initiateAuth !== 'function') {
            throw new Error('Provider must implement initiateAuth method');
        }
        if (typeof provider.getToken !== 'function') {
            throw new Error('Provider must implement getToken method');
        }
        if (typeof provider.refreshIfNeeded !== 'function') {
            throw new Error('Provider must implement refreshIfNeeded method');
        }
        this.providers.set(provider.name, provider);
        // CRITICAL FIX: Remove automatic OAuth provider initialization
        // OAuth providers should only initialize when actually needed
        // The "lazy initialization pattern" should be controlled by usage, not registration
        // This fixes issue 308 where OAuth was being initialized during MCP operations
    }
    /**
     * Get a registered OAuth provider
     * @param name - Provider name
     * @returns OAuth provider or undefined if not registered
     */
    getProvider(name) {
        return this.providers.get(name);
    }
    /**
     * Authenticate with a specific provider
     * @param providerName - Name of the provider to authenticate with
     */
    async authenticate(providerName) {
        if (!providerName || typeof providerName !== 'string') {
            throw new Error('Provider name must be a non-empty string');
        }
        const provider = this.providers.get(providerName);
        if (!provider) {
            throw new Error(`Unknown provider: ${providerName}`);
        }
        try {
            // 1. Initiate authentication with the provider
            await provider.initiateAuth();
            // 2. Get token from provider after successful auth
            const providerToken = await provider.getToken();
            if (!providerToken) {
                throw new Error('Authentication completed but no token was returned');
            }
            // 3. Store token using tokenStore
            await this.tokenStore.saveToken(providerName, providerToken);
        }
        catch (error) {
            // Propagate provider authentication errors
            if (error instanceof Error) {
                throw error;
            }
            throw new Error(`Authentication failed for provider ${providerName}: ${String(error)}`);
        }
    }
    /**
     * Get authentication status for all registered providers
     * @returns Array of authentication status for each provider
     */
    async getAuthStatus() {
        const statuses = [];
        // Get all registered providers and check their status
        for (const [providerName, _provider] of this.providers) {
            try {
                const token = await this.tokenStore.getToken(providerName);
                const oauthEnabled = this.isOAuthEnabled(providerName);
                if (token) {
                    // Provider is authenticated, calculate time until expiry
                    const now = Date.now() / 1000; // Convert to seconds to match token.expiry
                    const expiresIn = Math.max(0, Math.floor(token.expiry - now)); // seconds
                    statuses.push({
                        provider: providerName,
                        authenticated: true,
                        authType: 'oauth',
                        expiresIn,
                        oauthEnabled,
                    });
                }
                else {
                    // Provider is not authenticated
                    statuses.push({
                        provider: providerName,
                        authenticated: false,
                        authType: 'none',
                        oauthEnabled,
                    });
                }
            }
            catch (_error) {
                // If we can't get token status, consider it unauthenticated
                const oauthEnabled = this.isOAuthEnabled(providerName);
                statuses.push({
                    provider: providerName,
                    authenticated: false,
                    authType: 'none',
                    oauthEnabled,
                });
            }
        }
        return statuses;
    }
    /**
     * @plan PLAN-20250823-AUTHFIXES.P14
     * @requirement REQ-002
     * @pseudocode lines 51-68
     * Check if authenticated with a specific provider (required by precedence resolver)
     * @param providerName - Name of the provider
     * @returns True if authenticated, false otherwise
     */
    async isAuthenticated(providerName) {
        // Lines 52-55: VALIDATE providerName
        if (!providerName || typeof providerName !== 'string') {
            return false;
        }
        // Special handling for Gemini - if OAuth is enabled, assume authenticated
        // since the actual auth is handled by LOGIN_WITH_GOOGLE
        if (providerName === 'gemini' && this.isOAuthEnabled('gemini')) {
            return true;
        }
        // Lines 57-60: SET token = AWAIT this.tokenStore.getToken(providerName)
        const token = await this.tokenStore.getToken(providerName);
        if (!token) {
            return false;
        }
        // Lines 62-66: Check if token is expired
        const now = Date.now() / 1000;
        if (token.expiry <= now) {
            return false;
        }
        // Line 68: RETURN true
        return true;
    }
    /**
     * @plan PLAN-20250823-AUTHFIXES.P14
     * @requirement REQ-002.1
     * @pseudocode lines 4-37
     * Logout from a specific provider by clearing stored tokens
     * @param providerName - Name of the provider to logout from
     */
    async logout(providerName) {
        // Line 5-8: VALIDATE providerName
        if (!providerName || typeof providerName !== 'string') {
            throw new Error('Provider name must be a non-empty string');
        }
        // Line 10-13: Get provider
        const provider = this.providers.get(providerName);
        if (!provider) {
            throw new Error(`Unknown provider: ${providerName}`);
        }
        // Lines 16-26: Call provider logout if exists
        if ('logout' in provider && typeof provider.logout === 'function') {
            try {
                await provider.logout();
            }
            catch (error) {
                console.warn(`Provider logout failed:`, error);
            }
        }
        else {
            await this.tokenStore.removeToken(providerName);
        }
        // CRITICAL FIX: Clear all provider auth caches after logout
        // This ensures BaseProvider and specific provider caches are invalidated
        await this.clearProviderAuthCaches(providerName);
        // Special handling for Gemini - clear all Google OAuth related files
        if (providerName === 'gemini') {
            try {
                const fs = await import('fs/promises');
                const path = await import('path');
                const os = await import('os');
                const llxprtDir = path.join(os.homedir(), '.llxprt');
                // Clear the OAuth credentials
                const legacyCredsPath = path.join(llxprtDir, 'oauth_creds.json');
                try {
                    await fs.unlink(legacyCredsPath);
                    console.log('Cleared Gemini OAuth credentials');
                }
                catch {
                    // File might not exist
                }
                // Clear the Google accounts file
                const googleAccountsPath = path.join(llxprtDir, 'google_accounts.json');
                try {
                    await fs.unlink(googleAccountsPath);
                    console.log('Cleared Google account info');
                }
                catch {
                    // File might not exist
                }
                // Force the OAuth client to re-authenticate by clearing any cached state
                // The next request will need to re-authenticate
            }
            catch (error) {
                console.debug('Error clearing Gemini credentials:', error);
            }
        }
    }
    /**
     * @plan PLAN-20250823-AUTHFIXES.P14
     * @requirement REQ-002
     * @pseudocode lines 39-49
     * Logout from all providers by clearing all stored tokens
     */
    async logoutAll() {
        // Line 40: SET providers = AWAIT this.tokenStore.listProviders()
        const providers = await this.tokenStore.listProviders();
        // Lines 42-49: FOR EACH provider IN providers DO
        for (const provider of providers) {
            try {
                // Line 44: AWAIT this.logout(provider)
                await this.logout(provider);
            }
            catch (error) {
                // Lines 45-47: LOG "Failed to logout from " + provider + ": " + error
                console.warn(`Failed to logout from ${provider}: ${error}`);
                // Continue with other providers even if one fails
            }
        }
    }
    /**
     * Get OAuth token for a specific provider
     * Compatible with precedence resolver - returns access token string
     * @param providerName - Name of the provider
     * @returns Access token string if available, null otherwise
     */
    async getToken(providerName) {
        // Check if OAuth is enabled for this provider
        if (!this.isOAuthEnabled(providerName)) {
            return null;
        }
        const token = await this.getOAuthToken(providerName);
        // Special handling for different providers
        // @plan:PLAN-20250823-AUTHFIXES.P15
        // @requirement:REQ-004
        // Removed magic string handling for Gemini - now uses standard OAuth flow
        // For Qwen, return the OAuth token to be used as API key
        if (providerName === 'qwen' && token) {
            return token.access_token;
        }
        if (token) {
            return token.access_token;
        }
        // For other providers, trigger OAuth flow
        try {
            await this.authenticate(providerName);
            const newToken = await this.getOAuthToken(providerName);
            // Return the access token without any prefix - OAuth Bearer tokens should be used as-is
            return newToken ? newToken.access_token : null;
        }
        catch (error) {
            // Special handling for Gemini - USE_EXISTING_GEMINI_OAUTH is not an error
            // It's a signal to use the existing LOGIN_WITH_GOOGLE flow
            if (providerName === 'gemini' &&
                error instanceof Error &&
                error.message === 'USE_EXISTING_GEMINI_OAUTH') {
                // Return null to signal that OAuth should be handled by GeminiProvider
                return null;
            }
            // Re-throw the error so it's not silently swallowed
            console.error(`OAuth authentication failed for ${providerName}:`, error);
            throw error;
        }
    }
    /**
     * Get OAuth token object for a specific provider
     * @param providerName - Name of the provider
     * @returns OAuth token if available, null otherwise
     */
    async getOAuthToken(providerName) {
        if (!providerName || typeof providerName !== 'string') {
            throw new Error('Provider name must be a non-empty string');
        }
        const provider = this.providers.get(providerName);
        if (!provider) {
            throw new Error(`Unknown provider: ${providerName}`);
        }
        try {
            // 1. Try to get token from store
            const token = await this.tokenStore.getToken(providerName);
            if (!token) {
                return null;
            }
            // 2. Check if token expires within 30 seconds (30000ms)
            const now = Date.now();
            const thirtySecondsFromNow = now + 30000;
            if (token.expiry <= thirtySecondsFromNow) {
                // 3. Token is expired or about to expire, try refresh
                try {
                    const refreshedToken = await provider.refreshIfNeeded();
                    if (refreshedToken) {
                        // 4. Update stored token if refreshed
                        await this.tokenStore.saveToken(providerName, refreshedToken);
                        return refreshedToken;
                    }
                    else {
                        // Refresh failed, return null
                        return null;
                    }
                }
                catch (_error) {
                    // Token refresh failure: Return null, no logging
                    return null;
                }
            }
            // 5. Return valid token
            return token;
        }
        catch (error) {
            // For unknown provider or other critical errors, throw
            if (error instanceof Error &&
                error.message.includes('Unknown provider')) {
                throw error;
            }
            // For other errors, return null
            return null;
        }
    }
    /**
     * Get list of all registered provider names
     * @returns Array of provider names
     */
    getSupportedProviders() {
        return Array.from(this.providers.keys()).sort();
    }
    /**
     * Toggle OAuth enablement for a provider
     * @param providerName - Name of the provider
     * @returns New enablement state (true if enabled, false if disabled)
     */
    async toggleOAuthEnabled(providerName) {
        if (!providerName || typeof providerName !== 'string') {
            throw new Error('Provider name must be a non-empty string');
        }
        const provider = this.providers.get(providerName);
        if (!provider) {
            throw new Error(`Unknown provider: ${providerName}`);
        }
        const currentlyEnabled = this.isOAuthEnabled(providerName);
        const newState = !currentlyEnabled;
        if (this.settings) {
            // If we have settings, persist the state
            // Get current OAuth enabled providers or initialize empty object
            const oauthEnabledProviders = this.settings.merged.oauthEnabledProviders || {};
            oauthEnabledProviders[providerName] = newState;
            // Save the updated configuration
            this.settings.setValue(SettingScope.User, 'oauthEnabledProviders', oauthEnabledProviders);
        }
        else {
            // No settings available, store in memory only
            // This allows OAuth to work even without a settings file
            this.inMemoryOAuthState.set(providerName, newState);
        }
        return newState;
    }
    /**
     * Check if OAuth is enabled for a provider
     * @param providerName - Name of the provider
     * @returns True if OAuth is enabled, false otherwise
     */
    isOAuthEnabled(providerName) {
        if (this.settings) {
            // Check settings first if available
            const oauthEnabledProviders = this.settings.merged.oauthEnabledProviders || {};
            return oauthEnabledProviders[providerName] ?? false;
        }
        else {
            // Fall back to in-memory state if no settings
            return this.inMemoryOAuthState.get(providerName) ?? false;
        }
    }
    /**
     * Check for higher priority authentication methods
     * @param providerName - Name of the provider to check
     * @returns String describing higher priority auth method, null if none
     */
    async getHigherPriorityAuth(providerName) {
        if (!this.settings) {
            return null;
        }
        const merged = this.settings.merged;
        const settingsService = getSettingsService();
        const authOnly = isAuthOnlyEnabled(settingsService.get('authOnly'));
        if (authOnly) {
            return null;
        }
        // Check for API keys (highest priority)
        if (merged.providerApiKeys && merged.providerApiKeys[providerName]) {
            return 'API Key';
        }
        // Check for keyfiles (second highest priority)
        if (merged.providerKeyfiles && merged.providerKeyfiles[providerName]) {
            return 'Keyfile';
        }
        // Check for environment variables
        const envKeyName = `${providerName.toUpperCase()}_API_KEY`;
        if (process.env[envKeyName]) {
            return 'Environment Variable';
        }
        // For OpenAI-based providers, check if baseURL is compatible
        if (providerName === 'qwen') {
            const baseUrls = merged.providerBaseUrls || {};
            const openaiBaseUrl = baseUrls['openai'];
            if (openaiBaseUrl && !this.isQwenCompatibleUrl(openaiBaseUrl)) {
                return 'OpenAI BaseURL Mismatch';
            }
        }
        return null;
    }
    /**
     * Check if a URL is compatible with Qwen OAuth
     * @param url - The base URL to check
     * @returns True if compatible, false otherwise
     */
    isQwenCompatibleUrl(url) {
        if (!url)
            return true; // Default OpenAI endpoint is compatible
        // Qwen-compatible URLs
        const qwenDomains = ['dashscope.aliyuncs.com', 'qwen.com', 'api.qwen.com'];
        try {
            const urlObj = new URL(url);
            return qwenDomains.some((domain) => urlObj.hostname.includes(domain));
        }
        catch {
            return false; // Invalid URL format
        }
    }
    /**
     * CRITICAL FIX: Clear all auth caches for a provider after logout
     * This method finds and clears auth caches from both BaseProvider and provider-specific implementations
     * @param providerName - Name of the provider to clear caches for
     */
    async clearProviderAuthCaches(providerName) {
        try {
            // Import ProviderManager to access active providers
            // Use dynamic import to avoid circular dependencies
            const { getProviderManager } = await import('../providers/providerManagerInstance.js');
            const providerManager = getProviderManager();
            // Get the provider instance to clear its auth cache
            const targetProvider = providerManager.getProviderByName(providerName);
            if (targetProvider) {
                // Clear BaseProvider auth cache
                if (typeof targetProvider.clearAuthCache === 'function') {
                    targetProvider.clearAuthCache();
                }
                // Clear provider-specific cached clients
                // For AnthropicProvider: clear _cachedAuthKey
                if ('_cachedAuthKey' in targetProvider) {
                    const provider = targetProvider;
                    provider._cachedAuthKey = undefined;
                }
                // For GeminiProvider: clear any auth-related state
                if (targetProvider.name === 'gemini') {
                    // Clear GeminiProvider auth state
                    if ('clearAuthCache' in targetProvider &&
                        typeof targetProvider.clearAuthCache === 'function') {
                        targetProvider.clearAuthCache();
                    }
                    // Clear any client instances that might be cached
                    if ('authMode' in targetProvider) {
                        const geminiProvider = targetProvider;
                        geminiProvider.authMode = 'none';
                    }
                }
                // For providers extending BaseProvider: ensure auth cache is cleared
                if ('clearState' in targetProvider &&
                    typeof targetProvider.clearState === 'function') {
                    targetProvider.clearState();
                }
            }
            console.debug(`Cleared auth caches for provider: ${providerName}`);
        }
        catch (error) {
            // Cache clearing failures should not prevent logout from succeeding
            console.debug(`Failed to clear provider auth caches for ${providerName}:`, error);
        }
    }
}
//# sourceMappingURL=oauth-manager.js.map