/**
 * @license
 * Copyright 2025 Vybestack LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { SettingScope } from '../config/settings.js';
import { BucketFailoverHandlerImpl } from './BucketFailoverHandlerImpl.js';
import { getSettingsService, flushRuntimeAuthScope, DebugLogger, } from '@vybestack/llxprt-code-core';
const logger = new DebugLogger('llxprt:oauth:manager');
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;
}
function isLoggingWrapperCandidate(provider) {
    return (!!provider &&
        typeof provider === 'object' &&
        Object.prototype.hasOwnProperty.call(provider, 'wrappedProvider'));
}
/**
 * @plan PLAN-20251020-STATELESSPROVIDER3.P12
 * @requirement REQ-SP3-003
 * @pseudocode oauth-safety.md lines 1-17
 */
export function unwrapLoggingProvider(provider) {
    if (!provider) {
        return provider;
    }
    const visited = new Set();
    let current = provider;
    while (isLoggingWrapperCandidate(current)) {
        if (visited.has(current)) {
            break;
        }
        visited.add(current);
        const next = current.wrappedProvider;
        if (!next) {
            break;
        }
        current = next;
    }
    return current;
}
/**
 * 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;
    // Session bucket overrides (in-memory only)
    sessionBuckets;
    bucketResolutionLocks;
    // Getter function for message bus (lazy resolution for TUI prompts)
    getMessageBus;
    // Getter function for config (lazy resolution for bucket failover handler)
    getConfig;
    constructor(tokenStore, settings) {
        this.providers = new Map();
        this.tokenStore = tokenStore;
        this.settings = settings;
        this.inMemoryOAuthState = new Map();
        this.sessionBuckets = new Map();
        this.bucketResolutionLocks = new Map();
    }
    async withBucketResolutionLock(providerName, fn) {
        const currentTail = this.bucketResolutionLocks.get(providerName);
        const safeTail = currentTail?.catch(() => undefined) ?? Promise.resolve();
        let release;
        const next = new Promise((resolve) => {
            release = resolve;
        });
        const nextTail = safeTail.then(() => next);
        this.bucketResolutionLocks.set(providerName, nextTail);
        await safeTail;
        try {
            return await fn();
        }
        finally {
            release?.();
            if (this.bucketResolutionLocks.get(providerName) === nextTail) {
                this.bucketResolutionLocks.delete(providerName);
            }
        }
    }
    /**
     * Set the message bus getter for interactive TUI prompts
     * Uses a getter function to enable lazy resolution after TUI is initialized
     * @param getter - Function that returns the message bus instance from Config
     */
    setMessageBus(getter) {
        this.getMessageBus = getter;
    }
    /**
     * Set the config getter for bucket failover handler setup
     * Uses a getter function to enable lazy resolution
     * @plan PLAN-20251213issue490
     * @param getter - Function that returns the Config instance
     */
    setConfigGetter(getter) {
        this.getConfig = getter;
    }
    /**
     * 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
     * @param bucket - Optional bucket name for multi-account support
     */
    async authenticate(providerName, bucket) {
        logger.debug(() => `[FLOW] authenticate() called for provider: ${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
            logger.debug(() => `[FLOW] Calling provider.initiateAuth() for ${providerName}...`);
            await provider.initiateAuth();
            logger.debug(() => `[FLOW] provider.initiateAuth() completed for ${providerName}`);
            // 2. Get token from provider after successful auth
            logger.debug(() => `[FLOW] Calling provider.getToken() for ${providerName}...`);
            const providerToken = await provider.getToken();
            if (!providerToken) {
                logger.debug(() => `[FLOW] provider.getToken() returned null for ${providerName}!`);
                throw new Error('Authentication completed but no token was returned');
            }
            logger.debug(() => `[FLOW] provider.getToken() returned token for ${providerName}: access_token=${String(providerToken.access_token).substring(0, 10)}...`);
            // 3. Store token using tokenStore with bucket parameter
            logger.debug(() => `[FLOW] Saving token to tokenStore for ${providerName}...`);
            await this.tokenStore.saveToken(providerName, providerToken, bucket);
            logger.debug(() => `[FLOW] Token saved to tokenStore for ${providerName}`);
            // 4. Ensure provider marked as OAuth enabled after successful auth
            if (!this.isOAuthEnabled(providerName)) {
                logger.debug(() => `[FLOW] Enabling OAuth for ${providerName}`);
                this.setOAuthEnabledState(providerName, true);
            }
            logger.debug(() => `[FLOW] authenticate() completed successfully for ${providerName}`);
        }
        catch (error) {
            logger.debug(() => `[FLOW] authenticate() FAILED for ${providerName}: ${error instanceof Error ? error.message : 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 oauthEnabled = this.isOAuthEnabled(providerName);
                if (!oauthEnabled) {
                    statuses.push({
                        provider: providerName,
                        authenticated: false,
                        authType: 'none',
                        oauthEnabled,
                    });
                    continue;
                }
                const token = await this.tokenStore.getToken(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
     * @param bucket - Optional bucket name
     * @returns True if authenticated, false otherwise
     */
    async isAuthenticated(providerName, bucket) {
        // 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, bucket)
        const token = await this.tokenStore.getToken(providerName, bucket);
        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
     * @param bucket - Optional bucket name for multi-account support
     */
    async logout(providerName, bucket) {
        // 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}`);
        }
        // Resolve which bucket to act on (explicit bucket > session bucket > default)
        const bucketToUse = bucket ?? this.sessionBuckets.get(providerName) ?? 'default';
        // Call provider logout if exists (best-effort remote revoke), but ALWAYS clear local token
        if ('logout' in provider && typeof provider.logout === 'function') {
            try {
                // NOTE: Provider logout implementations are not bucket-aware today.
                // Local bucket cleanup is handled below via tokenStore.
                await provider.logout();
            }
            catch (error) {
                logger.warn(`Provider logout failed:`, error);
            }
        }
        await this.tokenStore.removeToken(providerName, bucketToUse);
        // If we just logged out the active session bucket, clear the in-memory override.
        if (this.sessionBuckets.get(providerName) === bucketToUse) {
            this.clearSessionBucket(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);
                    logger.debug('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);
                    logger.debug('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) {
                logger.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
                logger.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
     * @param _metadata - Optional metadata for token request (unused in CLI implementation)
     * @returns Access token string if available, null otherwise
     */
    async getToken(providerName, bucket) {
        logger.debug(() => `[FLOW] getToken() called for provider: ${providerName}`);
        // Check if OAuth is enabled for this provider
        if (!this.isOAuthEnabled(providerName)) {
            logger.debug(() => `[FLOW] OAuth is NOT enabled for ${providerName}, returning null`);
            return null;
        }
        logger.debug(() => `[FLOW] OAuth is enabled for ${providerName}, calling getOAuthToken()...`);
        const token = await this.getOAuthToken(providerName, bucket);
        // 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) {
            logger.debug(() => `[FLOW] Returning Qwen token: ${token.access_token.substring(0, 10)}...`);
            return token.access_token;
        }
        if (token) {
            logger.debug(() => `[FLOW] Returning existing token for ${providerName}: ${token.access_token.substring(0, 10)}...`);
            return token.access_token;
        }
        // For other providers, trigger OAuth flow
        // Check if the current profile has multiple buckets - if so, use MultiBucketAuthenticator
        logger.debug(() => `[FLOW] No existing token for ${providerName}, triggering OAuth flow...`);
        try {
            const buckets = await this.getProfileBuckets(providerName);
            if (buckets.length > 1) {
                logger.debug(`Multi-bucket lazy auth triggered for ${providerName} with ${buckets.length} buckets`);
                await this.authenticateMultipleBuckets(providerName, buckets);
            }
            else {
                await this.authenticate(providerName);
            }
            logger.debug(() => `[FLOW] authenticate() completed for ${providerName}, fetching new token...`);
            const newToken = await this.getOAuthToken(providerName);
            // Return the access token without any prefix - OAuth Bearer tokens should be used as-is
            if (newToken) {
                logger.debug(() => `[FLOW] Returning new token for ${providerName}: ${newToken.access_token.substring(0, 10)}...`);
            }
            else {
                logger.debug(() => `[FLOW] getOAuthToken() returned null after authenticate() for ${providerName}`);
            }
            return newToken ? newToken.access_token : null;
        }
        catch (error) {
            logger.debug(() => `[FLOW] getToken() OAuth flow FAILED for ${providerName}: ${error instanceof Error ? error.message : 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
            logger.error(`OAuth authentication failed for ${providerName}:`, error);
            throw error;
        }
    }
    /**
     * Retrieve the stored OAuth token without refreshing it.
     * Returns null if the provider is unknown or no token exists.
     */
    async peekStoredToken(providerName) {
        if (!providerName || typeof providerName !== 'string') {
            throw new Error('Provider name must be a non-empty string');
        }
        if (!this.providers.has(providerName)) {
            throw new Error(`Unknown provider: ${providerName}`);
        }
        try {
            return await this.tokenStore.getToken(providerName);
        }
        catch (error) {
            logger.debug(`Failed to load stored token for ${providerName}:`, error);
            return null;
        }
    }
    /**
     * Get OAuth token object for a specific provider
     * @param providerName - Name of the provider
     * @param bucket - Optional bucket name for multi-account support (if string), or metadata (for backward compatibility)
     * @returns OAuth token if available, null otherwise
     */
    async getOAuthToken(providerName, bucket) {
        logger.debug(() => `[FLOW] getOAuthToken() called for provider: ${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 explicitBucket = typeof bucket === 'string';
        // Determine the bucket to use: explicit bucket parameter or session bucket override
        let bucketToUse;
        if (explicitBucket) {
            bucketToUse = bucket;
        }
        // If no explicit/session bucket is set, fall back to the first bucket in the active profile.
        // This enables multi-bucket profiles to work without requiring an explicit /auth <provider> switch.
        let profileBuckets = [];
        let failoverHandler;
        if (!explicitBucket) {
            await this.withBucketResolutionLock(providerName, async () => {
                if (this.sessionBuckets.has(providerName)) {
                    bucketToUse = this.sessionBuckets.get(providerName);
                }
                profileBuckets = await this.getProfileBuckets(providerName);
                const config = this.getConfig?.();
                if (config && profileBuckets.length > 1) {
                    failoverHandler = config.getBucketFailoverHandler?.();
                    const existingBuckets = failoverHandler?.getBuckets?.() ?? [];
                    const sameBuckets = existingBuckets.length === profileBuckets.length &&
                        existingBuckets.every((value, index) => value === profileBuckets[index]);
                    if (!failoverHandler || !sameBuckets) {
                        const handler = new BucketFailoverHandlerImpl(profileBuckets, providerName, this);
                        config.setBucketFailoverHandler(handler);
                        failoverHandler = handler;
                    }
                }
                if (!bucketToUse) {
                    const handlerBucket = failoverHandler?.getCurrentBucket?.();
                    if (typeof handlerBucket === 'string' &&
                        handlerBucket.trim() !== '') {
                        bucketToUse = handlerBucket;
                    }
                    else if (profileBuckets.length > 0) {
                        bucketToUse = profileBuckets[0];
                    }
                    // Establish a default session bucket for the duration of this CLI session.
                    if (bucketToUse && !this.sessionBuckets.has(providerName)) {
                        this.sessionBuckets.set(providerName, bucketToUse);
                    }
                }
            });
        }
        try {
            // 1. Try to get token from store with bucket parameter
            logger.debug(() => `[FLOW] Reading token from tokenStore for ${providerName}...`);
            const token = await this.tokenStore.getToken(providerName, bucketToUse);
            if (!token) {
                logger.debug(() => `[FLOW] No token in tokenStore for ${providerName}`);
                return null;
            }
            logger.debug(() => `[FLOW] Token found in tokenStore for ${providerName}: expiry=${token.expiry}, keys=${Object.keys(token).join(',')}`);
            // 2. Check if token expires within 30 seconds
            const nowInSeconds = Math.floor(Date.now() / 1000);
            const thirtySecondsFromNow = nowInSeconds + 30;
            logger.debug(() => `[FLOW] Token expiry check: now=${nowInSeconds}, expiry=${token.expiry}, isExpired=${token.expiry <= thirtySecondsFromNow}`);
            if (token.expiry <= thirtySecondsFromNow) {
                // 3. Token is expired or about to expire, try refresh
                logger.debug(() => `[FLOW] Token expired or expiring soon for ${providerName}, attempting refresh...`);
                try {
                    const refreshedToken = await provider.refreshIfNeeded();
                    if (refreshedToken) {
                        // 4. Update stored token if refreshed
                        logger.debug(() => `[FLOW] Token refreshed for ${providerName}, saving to store...`);
                        await this.tokenStore.saveToken(providerName, refreshedToken, bucketToUse);
                        return refreshedToken;
                    }
                    else {
                        // Refresh failed, return null
                        logger.debug(() => `[FLOW] Token refresh returned null for ${providerName}`);
                        return null;
                    }
                }
                catch (refreshError) {
                    // Token refresh failure: Return null, no logging
                    logger.debug(() => `[FLOW] Token refresh FAILED for ${providerName}: ${refreshError instanceof Error ? refreshError.message : refreshError}`);
                    return null;
                }
            }
            // 5. Return valid token
            logger.debug(() => `[FLOW] Returning valid token for ${providerName}`);
            return token;
        }
        catch (error) {
            logger.debug(() => `[FLOW] getOAuthToken() ERROR for ${providerName}: ${error instanceof Error ? error.message : 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;
        this.setOAuthEnabledState(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;
        }
    }
    setOAuthEnabledState(providerName, enabled) {
        if (this.settings) {
            const oauthEnabledProviders = this.settings.merged.oauthEnabledProviders || {};
            oauthEnabledProviders[providerName] = enabled;
            this.settings.setValue(SettingScope.User, 'oauthEnabledProviders', oauthEnabledProviders);
        }
        else {
            this.inMemoryOAuthState.set(providerName, enabled);
        }
    }
    getTokenStore() {
        return this.tokenStore;
    }
    /**
     * 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
     *
     * @plan PLAN-20251020-STATELESSPROVIDER3.P12
     * @requirement REQ-SP3-003
     * @pseudocode oauth-safety.md lines 1-17
     */
    async clearProviderAuthCaches(providerName) {
        try {
            // Import ProviderManager to access active providers
            // Use dynamic import to avoid circular dependencies
            const { getCliProviderManager, getCliRuntimeContext } = await import('../runtime/runtimeSettings.js');
            const providerManager = getCliProviderManager();
            // Get the provider instance to clear its auth cache
            const targetProvider = providerManager.getProviderByName(providerName);
            const provider = unwrapLoggingProvider(targetProvider);
            if (!provider) {
                logger.debug(`Provider ${providerName} is not registered; skipping auth cache clear.`);
                return;
            }
            if ('clearAuthCache' in provider &&
                typeof provider.clearAuthCache === 'function') {
                provider.clearAuthCache();
            }
            if ('_cachedAuthKey' in provider) {
                const providerWithAuthKey = provider;
                providerWithAuthKey._cachedAuthKey = undefined;
            }
            if (provider.name === 'gemini') {
                if ('clearAuthCache' in provider &&
                    typeof provider.clearAuthCache === 'function') {
                    provider.clearAuthCache();
                }
                if ('clearAuth' in provider &&
                    typeof provider.clearAuth === 'function') {
                    provider.clearAuth();
                }
            }
            if ('clearState' in provider &&
                typeof provider.clearState === 'function') {
                provider.clearState();
            }
            try {
                const runtimeContext = getCliRuntimeContext();
                if (runtimeContext && typeof runtimeContext.runtimeId === 'string') {
                    flushRuntimeAuthScope(runtimeContext.runtimeId);
                }
            }
            catch (runtimeError) {
                logger.debug(`Skipped runtime auth scope flush for ${providerName}:`, runtimeError);
            }
            logger.debug(`Cleared auth caches for provider: ${providerName}`);
        }
        catch (error) {
            // Cache clearing failures should not prevent logout from succeeding
            logger.debug(`Failed to clear provider auth caches for ${providerName}:`, error);
        }
    }
    /**
     * Set session bucket override for a provider
     * Session state is in-memory only and not persisted
     */
    setSessionBucket(provider, bucket) {
        this.sessionBuckets.set(provider, bucket);
    }
    /**
     * Get session bucket override for a provider
     * Returns undefined if no session override set
     */
    getSessionBucket(provider) {
        return this.sessionBuckets.get(provider);
    }
    /**
     * Clear session bucket override for a provider
     */
    clearSessionBucket(provider) {
        this.sessionBuckets.delete(provider);
    }
    /**
     * List all buckets for a provider
     */
    async listBuckets(provider) {
        return this.tokenStore.listBuckets(provider);
    }
    /**
     * Logout from all buckets for a provider
     */
    async logoutAllBuckets(provider) {
        const buckets = await this.tokenStore.listBuckets(provider);
        for (const bucket of buckets) {
            try {
                await this.logout(provider, bucket);
            }
            catch (error) {
                logger.warn(`Failed to logout from bucket ${bucket}:`, error);
            }
        }
        this.clearSessionBucket(provider);
    }
    /**
     * Get authentication status with bucket information
     */
    async getAuthStatusWithBuckets(provider) {
        const buckets = await this.tokenStore.listBuckets(provider);
        const sessionBucket = this.sessionBuckets.get(provider);
        const statuses = [];
        for (const bucket of buckets) {
            const token = await this.tokenStore.getToken(provider, bucket);
            const isSessionBucket = bucket === sessionBucket;
            if (token) {
                statuses.push({
                    bucket,
                    authenticated: true,
                    expiry: token.expiry,
                    isSessionBucket,
                });
            }
            else {
                statuses.push({
                    bucket,
                    authenticated: false,
                    isSessionBucket,
                });
            }
        }
        return statuses;
    }
    /**
     * Get the list of buckets for the current profile for a given provider
     * If the current profile has auth.buckets configured, return those.
     * Otherwise, return empty array (single-bucket or non-OAuth profile)
     */
    async getProfileBuckets(providerName) {
        try {
            // Try to get profile from runtime settings
            const { getCliRuntimeServices } = await import('../runtime/runtimeSettings.js');
            const { settingsService } = getCliRuntimeServices();
            // Get current profile name
            const currentProfileName = typeof settingsService.getCurrentProfileName === 'function'
                ? settingsService.getCurrentProfileName()
                : settingsService.get('currentProfile');
            if (!currentProfileName) {
                return [];
            }
            // Load the profile to check for auth.buckets
            const { ProfileManager } = await import('@vybestack/llxprt-code-core');
            const profileManager = new ProfileManager();
            const profile = await profileManager.loadProfile(currentProfileName);
            // Check if profile has auth.buckets for this provider
            if ('auth' in profile &&
                profile.auth &&
                typeof profile.auth === 'object' &&
                'type' in profile.auth &&
                profile.auth.type === 'oauth' &&
                'buckets' in profile.auth &&
                Array.isArray(profile.auth.buckets)) {
                return profile.auth.buckets;
            }
            return [];
        }
        catch (error) {
            logger.debug(`Could not load profile buckets for ${providerName}:`, error);
            return [];
        }
    }
    /**
     * Authenticate multiple OAuth buckets sequentially using MultiBucketAuthenticator
     * with timing controls (delay/prompt) and browser auto-open settings
     */
    async authenticateMultipleBuckets(providerName, buckets) {
        const { MultiBucketAuthenticator } = await import('./MultiBucketAuthenticator.js');
        // Get ephemeral settings for timing controls
        const { getEphemeralSetting: getRuntimeEphemeralSetting } = await import('../runtime/runtimeSettings.js');
        const getEphemeralSetting = (key) => getRuntimeEphemeralSetting(key);
        // Debug: log the raw setting value
        const rawBucketPrompt = getRuntimeEphemeralSetting('auth-bucket-prompt');
        logger.debug('Checking auth-bucket-prompt setting', {
            rawValue: rawBucketPrompt,
            typeof: typeof rawBucketPrompt,
        });
        // Callback to authenticate a single bucket
        const onAuthBucket = async (provider, bucket, index, total) => {
            // Show visible console output for user
            console.log(`\n=== Bucket ${index} of ${total}: ${bucket} ===\n`);
            logger.debug(`Authenticating bucket ${index} of ${total}: ${bucket}`);
            await this.authenticate(provider, bucket);
        };
        // Callback for prompting - uses TUI dialog if available, falls back to delay
        const onPrompt = async (provider, bucket) => {
            // Try interactive TUI prompt if message bus getter is available
            // Use lazy resolution to get message bus after TUI is initialized
            const messageBus = this.getMessageBus?.();
            if (messageBus) {
                try {
                    logger.debug('Requesting bucket auth confirmation via message bus', {
                        provider,
                        bucket,
                    });
                    // Request confirmation via message bus with a timeout
                    // If TUI is ready, it will respond; otherwise we fall back to delay
                    const confirmPromise = messageBus.requestBucketAuthConfirmation(provider, bucket, buckets.indexOf(bucket) + 1, buckets.length);
                    // Race with a 3-second timeout to give TUI time to respond
                    const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), 3000));
                    const result = await Promise.race([confirmPromise, timeoutPromise]);
                    if (result !== 'timeout') {
                        // TUI responded - return the confirmation result
                        logger.debug('TUI responded to bucket auth confirmation', {
                            result,
                        });
                        return result;
                    }
                    // TUI didn't respond in time - fall back to delay
                    logger.debug('TUI not ready, falling back to delay-based prompt');
                }
                catch (error) {
                    logger.debug('Error using message bus, falling back to delay', {
                        error,
                    });
                }
            }
            else {
                logger.debug('No message bus available');
            }
            // TUI not available or timed out - try stdin if TTY and prompt enabled
            const showPrompt = getEphemeralSetting('auth-bucket-prompt');
            logger.debug('Checking TTY prompt fallback', {
                showPrompt,
                isTTY: process.stdin.isTTY,
            });
            if (showPrompt && process.stdin.isTTY) {
                // Interactive terminal - wait for keypress
                console.log(`\nReady to authenticate bucket: ${bucket}`);
                console.log('Press ENTER to continue, or Ctrl+C to cancel...\n');
                let rawModeSet = false;
                try {
                    await new Promise((resolve, reject) => {
                        const cleanup = () => {
                            process.stdin.removeListener('data', onData);
                            process.stdin.removeListener('error', onError);
                            if (rawModeSet && process.stdin.isTTY) {
                                process.stdin.setRawMode(false);
                            }
                            process.stdin.pause();
                        };
                        const onData = () => {
                            cleanup();
                            resolve();
                        };
                        const onError = (err) => {
                            cleanup();
                            reject(err);
                        };
                        if (process.stdin.isTTY) {
                            process.stdin.setRawMode(true);
                            rawModeSet = true;
                        }
                        process.stdin.resume();
                        process.stdin.once('data', onData);
                        process.stdin.once('error', onError);
                    });
                }
                catch (error) {
                    // Ensure raw mode is reset even on unexpected errors
                    if (rawModeSet && process.stdin.isTTY) {
                        try {
                            process.stdin.setRawMode(false);
                        }
                        catch {
                            // Ignore cleanup errors
                        }
                    }
                    throw error;
                }
                return true;
            }
            // Fall back to delay-based prompting (no TTY or prompt disabled)
            const delay = getEphemeralSetting('auth-bucket-delay') ?? 5000;
            console.log(`\nReady to authenticate bucket: ${bucket}`);
            console.log(`(waiting ${delay / 1000} seconds - switch browser window if needed...)\n`);
            await new Promise((resolve) => setTimeout(resolve, delay));
            return true;
        };
        // Callback for delay with visible console output
        const onDelay = async (ms, bucket) => {
            // Show visible console output for user
            console.log(`(waiting ${ms / 1000} seconds before opening browser...)\n`);
            logger.debug(`Waiting ${ms}ms before authenticating bucket: ${bucket}`);
            await new Promise((resolve) => setTimeout(resolve, ms));
        };
        const authenticator = MultiBucketAuthenticator.fromCallbacks({
            onAuthBucket,
            onPrompt,
            onDelay,
            getEphemeralSetting,
        });
        const result = await authenticator.authenticateMultipleBuckets({
            provider: providerName,
            buckets,
        });
        if (result.cancelled) {
            throw new Error(`Multi-bucket authentication cancelled after ${result.authenticatedBuckets.length} of ${buckets.length} buckets`);
        }
        if (result.failedBuckets.length > 0) {
            throw new Error(`Failed to authenticate ${result.failedBuckets.length} bucket(s): ${result.failedBuckets.join(', ')}`);
        }
        logger.debug(`Successfully authenticated ${result.authenticatedBuckets.length} buckets for ${providerName}`);
        // Set up bucket failover handler if we have multiple buckets and config is available
        // @plan PLAN-20251213issue490
        if (buckets.length > 1) {
            const config = this.getConfig?.();
            if (config) {
                const handler = new BucketFailoverHandlerImpl(buckets, providerName, this);
                config.setBucketFailoverHandler(handler);
                logger.debug('Bucket failover handler configured', {
                    provider: providerName,
                    bucketCount: buckets.length,
                });
            }
            else {
                logger.debug('Config not available, bucket failover handler not configured');
            }
        }
    }
}
//# sourceMappingURL=oauth-manager.js.map