/**
 * @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, mergeRefreshedToken, } from '@vybestack/llxprt-code-core';
const logger = new DebugLogger('llxprt:oauth:manager');
/** Maximum consecutive proactive renewal failures before stopping retries. */
const MAX_PROACTIVE_RENEWAL_FAILURES = 3;
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;
}
let profileManagerCtorPromise;
async function getProfileManagerCtor() {
    if (!profileManagerCtorPromise) {
        profileManagerCtorPromise = import('@vybestack/llxprt-code-core')
            .then((mod) => mod.ProfileManager)
            .catch((error) => {
            profileManagerCtorPromise = undefined;
            throw error;
        });
    }
    return profileManagerCtorPromise;
}
async function createProfileManager() {
    const ProfileManager = await getProfileManagerCtor();
    return new ProfileManager();
}
function isLoadBalancerProfileLike(profile) {
    return (!!profile &&
        typeof profile === 'object' &&
        'type' in profile &&
        profile.type === 'loadbalancer' &&
        'profiles' in profile &&
        Array.isArray(profile.profiles) &&
        profile.profiles.every((name) => typeof name === 'string' && name.trim() !== ''));
}
function getOAuthBucketsFromProfile(profile) {
    if (!profile || typeof profile !== 'object') {
        return null;
    }
    const providerName = 'provider' in profile && typeof profile.provider === 'string'
        ? profile.provider
        : null;
    if (!providerName || providerName.trim() === '') {
        return null;
    }
    const auth = 'auth' in profile ? profile.auth : undefined;
    if (!auth || typeof auth !== 'object') {
        return null;
    }
    if (!('type' in auth) || auth.type !== 'oauth') {
        return null;
    }
    const buckets = (() => {
        if ('buckets' in auth && Array.isArray(auth.buckets)) {
            const bucketNames = auth.buckets
                .filter((bucket) => typeof bucket === 'string')
                .map((bucket) => bucket.trim())
                .filter((bucket) => bucket !== '');
            if (bucketNames.length > 0) {
                return bucketNames;
            }
        }
        return ['default'];
    })();
    return { providerName: providerName.trim(), buckets };
}
/**
 * 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;
    proactiveRenewals;
    proactiveRenewalFailures;
    proactiveRenewalInFlight;
    proactiveRenewalTokens;
    // Getter function for message bus (lazy resolution for TUI prompts)
    getMessageBus;
    // Getter function for config (lazy resolution for bucket failover handler)
    getConfig;
    // Session-scoped flag: user dismissed the BucketAuthConfirmation dialog.
    // When true, subsequent auth attempts skip the dialog and proceed directly
    // (i.e. "don't bother me again" rather than "block auth").
    userDismissedAuthPrompt = false;
    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();
        this.proactiveRenewals = new Map();
        this.proactiveRenewalFailures = new Map();
        this.proactiveRenewalInFlight = new Set();
        this.proactiveRenewalTokens = 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.refreshToken !== 'function') {
            throw new Error('Provider must implement refreshToken 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}`);
        }
        // Acquire auth lock to prevent concurrent authentication for same provider+bucket
        const lockAcquired = await this.tokenStore.acquireAuthLock(providerName, {
            waitMs: 60000, // Wait up to 60 seconds
            staleMs: 360000, // Break locks older than 6 minutes
            bucket,
        });
        if (!lockAcquired) {
            // Lock timeout - check if another process completed auth
            const diskToken = await this.tokenStore.getToken(providerName, bucket);
            const nowInSeconds = Math.floor(Date.now() / 1000);
            const thirtySecondsFromNow = nowInSeconds + 30;
            if (diskToken && diskToken.expiry > thirtySecondsFromNow) {
                if (!this.isOAuthEnabled(providerName)) {
                    this.setOAuthEnabledState(providerName, true);
                }
                logger.debug(() => `[FLOW] Lock timeout but found valid token on disk for ${providerName}, using it`);
                return;
            }
            throw new Error(`Failed to acquire auth lock for ${providerName}${bucket ? `/${bucket}` : ''} and no valid token on disk`);
        }
        try {
            // Double-check disk for token written by another process while we waited
            const diskToken = await this.tokenStore.getToken(providerName, bucket);
            const nowInSeconds = Math.floor(Date.now() / 1000);
            const thirtySecondsFromNow = nowInSeconds + 30;
            if (diskToken && diskToken.expiry > thirtySecondsFromNow) {
                if (!this.isOAuthEnabled(providerName)) {
                    this.setOAuthEnabledState(providerName, true);
                }
                logger.debug(() => `[FLOW] Found valid token on disk after acquiring lock for ${providerName}, skipping auth`);
                return;
            }
            // @fix issue1652: Try token refresh before opening browser.
            // If the disk token is expired but has a valid refresh_token,
            // refreshing is cheaper and non-interactive — no browser needed.
            // Uses acquireRefreshLock to coordinate with getOAuthToken() refreshes
            // across processes — prevents replaying single-use refresh tokens.
            if (diskToken &&
                typeof diskToken.refresh_token === 'string' &&
                diskToken.refresh_token !== '') {
                const refreshLockAcquired = await this.tokenStore.acquireRefreshLock(providerName, {
                    waitMs: 10000,
                    staleMs: 30000,
                    bucket,
                });
                if (refreshLockAcquired) {
                    try {
                        // Re-read token under lock — another process may have refreshed it
                        const latestToken = (await this.tokenStore.getToken(providerName, bucket)) ??
                            diskToken;
                        const nowCheck = Math.floor(Date.now() / 1000);
                        if (latestToken.expiry > nowCheck + 30) {
                            if (!this.isOAuthEnabled(providerName)) {
                                this.setOAuthEnabledState(providerName, true);
                            }
                            logger.debug(() => `[FLOW] Another process refreshed token for ${providerName}/${bucket ?? 'default'}, skipping browser auth`);
                            return;
                        }
                        const refreshedToken = await provider.refreshToken(latestToken);
                        if (refreshedToken) {
                            const mergedToken = mergeRefreshedToken(latestToken, refreshedToken);
                            await this.tokenStore.saveToken(providerName, mergedToken, bucket);
                            if (!this.isOAuthEnabled(providerName)) {
                                this.setOAuthEnabledState(providerName, true);
                            }
                            logger.debug(() => `[FLOW] Refreshed expired token for ${providerName}/${bucket ?? 'default'}, skipping browser auth`);
                            return;
                        }
                    }
                    catch (refreshError) {
                        logger.debug(() => `[FLOW] Token refresh failed for ${providerName}/${bucket ?? 'default'}, falling through to browser auth: ${refreshError instanceof Error ? refreshError.message : refreshError}`);
                    }
                    finally {
                        await this.tokenStore.releaseRefreshLock(providerName, bucket);
                    }
                }
                else {
                    // Lock timeout — another process is likely refreshing right now.
                    // Re-read disk to see if they completed before opening browser.
                    const postLockToken = (await this.tokenStore.getToken(providerName, bucket)) ?? diskToken;
                    const nowPostLock = Math.floor(Date.now() / 1000);
                    if (postLockToken.expiry > nowPostLock + 30) {
                        if (!this.isOAuthEnabled(providerName)) {
                            this.setOAuthEnabledState(providerName, true);
                        }
                        logger.debug(() => `[FLOW] Another process refreshed token for ${providerName}/${bucket ?? 'default'} (detected after lock timeout), skipping browser auth`);
                        return;
                    }
                }
            }
            // Initiate authentication and get token directly
            logger.debug(() => `[FLOW] Calling provider.initiateAuth() for ${providerName}...`);
            const token = await provider.initiateAuth();
            logger.debug(() => `[FLOW] provider.initiateAuth() returned token for ${providerName}`);
            if (!token) {
                throw new Error('Authentication completed but no token was returned');
            }
            // Persist token with correct bucket
            logger.debug(() => `[FLOW] Saving token to tokenStore for ${providerName}...`);
            await this.tokenStore.saveToken(providerName, token, bucket);
            logger.debug(() => `[FLOW] Token saved to tokenStore for ${providerName}`);
            // 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)}`);
        }
        finally {
            // Always release the lock
            await this.tokenStore.releaseAuthLock(providerName, bucket);
            logger.debug(() => `[FLOW] Released auth lock for ${providerName}`);
        }
    }
    /**
     * 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,
                        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,
                        expiresIn,
                        oauthEnabled,
                    });
                }
                else {
                    // Provider is not authenticated
                    statuses.push({
                        provider: providerName,
                        authenticated: false,
                        oauthEnabled,
                    });
                }
            }
            catch (_error) {
                // If we can't get token status, consider it unauthenticated
                const oauthEnabled = this.isOAuthEnabled(providerName);
                statuses.push({
                    provider: providerName,
                    authenticated: false,
                    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';
        const tokenForLogout = await this.tokenStore.getToken(providerName, bucketToUse);
        // Call provider logout if exists (best-effort remote revoke), but ALWAYS clear local token
        if ('logout' in provider && typeof provider.logout === 'function') {
            try {
                if (tokenForLogout) {
                    await provider.logout(tokenForLogout);
                }
            }
            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}`);
        if (!this.providers.has(providerName)) {
            logger.debug(() => `[FLOW] Unknown provider for getToken(): ${providerName}`);
            return null;
        }
        // Respect explicit user settings when available.
        // If settings exist and OAuth is disabled for this provider, do not use stored
        // tokens and do not trigger OAuth flows.
        if (this.settings && !this.isOAuthEnabled(providerName)) {
            logger.debug(() => `[FLOW] OAuth is disabled by settings for ${providerName}, returning null`);
            return null;
        }
        // In runtimes without LoadedSettings, only block token reuse when there is
        // explicit in-memory state for this provider and that state is disabled.
        const hasExplicitInMemoryOAuthState = this.inMemoryOAuthState.has(providerName);
        if (!this.settings &&
            hasExplicitInMemoryOAuthState &&
            !this.isOAuthEnabled(providerName)) {
            logger.debug(() => `[FLOW] OAuth is disabled in-memory for ${providerName}, returning null`);
            return null;
        }
        // @fix issue1442: Try to get existing token BEFORE checking in-memory OAuth enablement.
        // This fixes subagents created without LoadedSettings: they can reuse existing tokens
        // instead of forcing unnecessary reauth loops.
        logger.debug(() => `[FLOW] Attempting to get existing token for ${providerName}...`);
        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;
        }
        // @fix issue1616: Peek other profile buckets for a valid token before triggering
        // full OAuth re-authentication. Unlike the previous approach (issue1191), this does
        // NOT call tryFailover() — that is reserved exclusively for RetryOrchestrator to
        // handle API-error-driven failover. This peek loop only reads the token store
        // directly without mutating any failover state.
        {
            const profileBuckets = await this.getProfileBuckets(providerName);
            if (profileBuckets.length > 1) {
                const nowInSeconds = Math.floor(Date.now() / 1000);
                const thirtySecondsFromNow = nowInSeconds + 30;
                const alreadyTriedBucket = this.getSessionBucket(providerName);
                for (const peekBucket of profileBuckets) {
                    if (peekBucket === alreadyTriedBucket)
                        continue;
                    try {
                        const peekToken = await this.tokenStore.getToken(providerName, peekBucket);
                        if (peekToken && peekToken.expiry > thirtySecondsFromNow) {
                            logger.debug(() => `[issue1616] Found valid token in bucket '${peekBucket}' for ${providerName}, switching session`);
                            this.setSessionBucket(providerName, peekBucket);
                            return peekToken.access_token;
                        }
                    }
                    catch (peekError) {
                        logger.debug(`[issue1616] Token peek failed for ${providerName}/${peekBucket}:`, peekError);
                    }
                }
                logger.debug(() => `[issue1616] No valid token found in any bucket for ${providerName}, falling through to OAuth`);
            }
        }
        // For other providers, trigger OAuth flow
        // Check if the current profile has multiple buckets - if so, use MultiBucketAuthenticator
        // Issue 913: Also check if auth-bucket-prompt is enabled for single/default buckets
        // @fix issue1442: Check OAuth enablement only when triggering NEW authentication.
        // In runtimes without LoadedSettings (e.g., isolated subagents), allow auth flow
        // when there's no explicit in-memory OAuth state yet.
        const shouldRequireOAuthEnabled = this.settings !== undefined || hasExplicitInMemoryOAuthState;
        if (shouldRequireOAuthEnabled && !this.isOAuthEnabled(providerName)) {
            logger.debug(() => `[FLOW] OAuth is NOT enabled for ${providerName}, cannot trigger new auth`);
            return null;
        }
        logger.debug(() => `[FLOW] No existing token for ${providerName}, triggering OAuth flow...`);
        // @fix issue1262 & issue1195: Before triggering OAuth, check disk with lock
        // Another process or earlier run may have written a valid token that we missed
        // Use the same locking pattern as PR #1258 to prevent race conditions
        const bucketToCheck = typeof bucket === 'string' ? bucket : undefined;
        const lockAcquired = await this.tokenStore.acquireRefreshLock(providerName, {
            waitMs: 5000, // Wait up to 5 seconds for lock
            staleMs: 30000,
            bucket: bucketToCheck,
        });
        if (lockAcquired) {
            try {
                // Double-check disk for token written by another process
                const diskToken = await this.tokenStore.getToken(providerName, bucketToCheck);
                const nowInSeconds = Math.floor(Date.now() / 1000);
                const thirtySecondsFromNow = nowInSeconds + 30;
                if (diskToken && diskToken.expiry > thirtySecondsFromNow) {
                    // Valid token found on disk! Use it instead of triggering OAuth
                    logger.debug(() => `[issue1262/1195] Found valid token on disk for ${providerName}, skipping OAuth`);
                    return diskToken.access_token;
                }
                // @fix issue1317: Expired disk token with refresh_token — attempt refresh
                // before falling through to full OAuth re-authentication
                if (diskToken &&
                    typeof diskToken.refresh_token === 'string' &&
                    diskToken.refresh_token !== '') {
                    const provider = this.providers.get(providerName);
                    if (provider) {
                        try {
                            const refreshedToken = await provider.refreshToken(diskToken);
                            if (refreshedToken) {
                                const mergedToken = mergeRefreshedToken(diskToken, refreshedToken);
                                await this.tokenStore.saveToken(providerName, mergedToken, bucketToCheck);
                                logger.debug(() => `[issue1317] Refreshed expired disk token for ${providerName}, skipping OAuth`);
                                return mergedToken.access_token;
                            }
                        }
                        catch (refreshError) {
                            logger.debug(() => `[issue1317] Disk token refresh failed for ${providerName}: ${refreshError instanceof Error ? refreshError.message : refreshError}`);
                        }
                    }
                }
            }
            finally {
                await this.tokenStore.releaseRefreshLock(providerName, bucketToCheck);
            }
        }
        else {
            // Couldn't acquire lock - check disk anyway, another process may have just written a token
            const diskToken = await this.tokenStore.getToken(providerName, bucketToCheck);
            const nowInSeconds = Math.floor(Date.now() / 1000);
            const thirtySecondsFromNow = nowInSeconds + 30;
            if (diskToken && diskToken.expiry > thirtySecondsFromNow) {
                logger.debug(() => `[issue1262/1195] Found valid token on disk after lock timeout for ${providerName}`);
                return diskToken.access_token;
            }
        }
        // @fix issue1616: For multi-bucket profiles, getToken() is a pure lookup.
        // Authentication is handled at the turn boundary via ensureBucketsAuthenticated().
        // For single-bucket or non-bucketed profiles, preserve existing auth behavior.
        try {
            const buckets = await this.getProfileBuckets(providerName);
            if (buckets.length > 1) {
                // Multi-bucket: pure lookup only — return null.
                // ensureBucketsAuthenticated() at turn boundary handles auth,
                // tryFailover() Pass 3 handles mid-turn reauth.
                return null;
            }
            // Single-bucket or no-bucket: preserve existing auth behavior
            // (Issue 913: auth-bucket-prompt routes through MultiBucketAuthenticator)
            let showPrompt = false;
            try {
                const { getEphemeralSetting: getRuntimeEphemeralSetting } = await import('../runtime/runtimeSettings.js');
                showPrompt =
                    getRuntimeEphemeralSetting('auth-bucket-prompt') ??
                        false;
            }
            catch (runtimeError) {
                logger.debug('Could not get ephemeral setting (runtime not initialized), using default', runtimeError);
            }
            if (showPrompt) {
                const effectiveBuckets = buckets.length === 1 ? buckets : ['default'];
                logger.debug(`Single-bucket auth with prompt mode for ${providerName}, bucket: ${effectiveBuckets[0]}`);
                await this.authenticateMultipleBuckets(providerName, effectiveBuckets);
            }
            else {
                await this.authenticate(providerName);
            }
            const newToken = await this.getOAuthToken(providerName);
            return newToken ? newToken.access_token : null;
        }
        catch (error) {
            // Special handling for Gemini - USE_EXISTING_GEMINI_OAUTH is not an error
            if (providerName === 'gemini' &&
                error instanceof Error &&
                error.message === 'USE_EXISTING_GEMINI_OAUTH') {
                return null;
            }
            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?.();
                // @fix issue1029 - Enhanced debug logging for failover handler setup
                logger.debug(() => `[issue1029] getOAuthToken: provider=${providerName}, buckets=${JSON.stringify(profileBuckets)}, hasConfig=${!!config}`);
                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]);
                    logger.debug(() => `[issue1029] Failover handler check: hasExisting=${!!failoverHandler}, sameBuckets=${sameBuckets}, existingBuckets=${JSON.stringify(existingBuckets)}`);
                    if (!failoverHandler || !sameBuckets) {
                        const handler = new BucketFailoverHandlerImpl(profileBuckets, providerName, this);
                        config.setBucketFailoverHandler(handler);
                        failoverHandler = handler;
                        logger.debug(() => `[issue1029] Created and set new BucketFailoverHandlerImpl on config for ${providerName} with buckets: ${JSON.stringify(profileBuckets)}`);
                    }
                }
                else if (profileBuckets.length > 1 && !config) {
                    // @fix issue1029 - This is the bug! We have multiple buckets but no config to set handler on
                    logger.warn(`[issue1029] CRITICAL: Profile has ${profileBuckets.length} buckets but no Config available to set failover handler! ` +
                        `Bucket failover will NOT work. Ensure OAuthManager.setConfigGetter is called with the active Config instance.`);
                }
                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 with locking
                logger.debug(() => `[FLOW] Token expired or expiring soon for ${providerName}, attempting refresh with lock...`);
                // Issue #1159: Acquire lock before refreshing to prevent concurrent refreshes
                const lockAcquired = await this.tokenStore.acquireRefreshLock(providerName, { waitMs: 10000, staleMs: 30000, bucket: bucketToUse });
                if (!lockAcquired) {
                    logger.debug(() => `[FLOW] Failed to acquire refresh lock for ${providerName}, checking disk...`);
                    // Lock timeout - check disk again in case another process refreshed
                    const reloadedToken = await this.tokenStore.getToken(providerName, bucketToUse);
                    if (reloadedToken && reloadedToken.expiry > thirtySecondsFromNow) {
                        logger.debug(() => `[FLOW] Token was refreshed by another process for ${providerName}`);
                        this.scheduleProactiveRenewal(providerName, bucketToUse, reloadedToken);
                        return reloadedToken;
                    }
                    // Still expired after lock timeout - return null
                    return null;
                }
                try {
                    // Issue #1159: Double-check pattern - re-read token after acquiring lock
                    const recheckToken = await this.tokenStore.getToken(providerName, bucketToUse);
                    if (recheckToken && recheckToken.expiry > thirtySecondsFromNow) {
                        logger.debug(() => `[FLOW] Token was refreshed by another process while waiting for lock for ${providerName}`);
                        this.scheduleProactiveRenewal(providerName, bucketToUse, recheckToken);
                        return recheckToken;
                    }
                    // Token is still expired, proceed with refresh
                    const refreshedToken = await provider.refreshToken(recheckToken || token);
                    if (refreshedToken) {
                        const mergedToken = mergeRefreshedToken((recheckToken || token), refreshedToken);
                        // 4. Update stored token if refreshed
                        logger.debug(() => `[FLOW] Token refreshed for ${providerName}, saving to store...`);
                        await this.tokenStore.saveToken(providerName, mergedToken, bucketToUse);
                        this.scheduleProactiveRenewal(providerName, bucketToUse, mergedToken);
                        return mergedToken;
                    }
                    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;
                }
                finally {
                    // Always release lock
                    await this.tokenStore.releaseRefreshLock(providerName, bucketToUse);
                }
            }
            // 5. Return valid token
            logger.debug(() => `[FLOW] Returning valid token for ${providerName}`);
            this.scheduleProactiveRenewal(providerName, bucketToUse, token);
            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;
        }
    }
    normalizeBucket(bucket) {
        if (typeof bucket === 'string' && bucket.trim() !== '') {
            return bucket;
        }
        return 'default';
    }
    getProactiveRenewalKey(providerName, bucket) {
        return `${providerName}:${bucket}`;
    }
    clearProactiveRenewal(key) {
        const entry = this.proactiveRenewals.get(key);
        if (entry) {
            clearTimeout(entry.timer);
            this.proactiveRenewals.delete(key);
        }
        this.proactiveRenewalFailures.delete(key);
        this.proactiveRenewalInFlight.delete(key);
        this.proactiveRenewalTokens.delete(key);
    }
    setProactiveTimer(providerName, bucket, delayMs, expiry) {
        const key = this.getProactiveRenewalKey(providerName, bucket);
        const existing = this.proactiveRenewals.get(key);
        if (existing) {
            clearTimeout(existing.timer);
        }
        const MAX_DELAY_MS = 2 ** 31 - 1;
        const safeDelay = Math.min(Math.max(0, delayMs), MAX_DELAY_MS);
        const timer = setTimeout(() => {
            void this.runProactiveRenewal(providerName, bucket).catch((error) => {
                logger.debug(() => `[OAUTH] Proactive renewal error for ${providerName}:${bucket}: ${error instanceof Error ? error.message : String(error)}`);
            });
        }, safeDelay);
        // Don't keep the process alive solely for renewals.
        if (typeof timer.unref === 'function') {
            timer.unref();
        }
        this.proactiveRenewals.set(key, { timer, expiry });
    }
    scheduleProactiveRetry(providerName, bucket) {
        const normalizedBucket = this.normalizeBucket(bucket);
        const key = this.getProactiveRenewalKey(providerName, normalizedBucket);
        const failures = (this.proactiveRenewalFailures.get(key) ?? 0) + 1;
        this.proactiveRenewalFailures.set(key, failures);
        // @plan PLAN-20260223-ISSUE1598.P14
        // @requirement REQ-1598-PR05
        // Stop retrying after MAX_PROACTIVE_RENEWAL_FAILURES consecutive failures
        if (failures >= MAX_PROACTIVE_RENEWAL_FAILURES) {
            logger.debug(() => `[OAUTH] Stopping proactive renewal after ${failures} failures for ${providerName}:${normalizedBucket}`);
            this.clearProactiveRenewal(key);
            return;
        }
        const cappedFailures = Math.min(failures, 10);
        const baseMs = 30_000;
        const delayMs = Math.min(baseMs * 2 ** cappedFailures, 30 * 60_000);
        const jitterMs = Math.floor(Math.random() * 5_000);
        const expiry = this.proactiveRenewals.get(key)?.expiry ?? 0;
        this.setProactiveTimer(providerName, normalizedBucket, delayMs + jitterMs, expiry);
    }
    /**
     * @plan:PLAN-20250214-CREDPROXY.P33
     * @requirement R16.8
     * @plan PLAN-20260223-ISSUE1598.P14
     * @requirement REQ-1598-PR01
     * @pseudocode proactive-renewal.md lines 15-49
     */
    scheduleProactiveRenewal(providerName, bucket, token) {
        // R16.8: Skip proactive renewal scheduling in proxy mode
        // The host process handles token refresh, not the sandbox
        if (process.env.LLXPRT_CREDENTIAL_SOCKET) {
            return;
        }
        if (!this.isOAuthEnabled(providerName)) {
            return;
        }
        if (!token.refresh_token || token.refresh_token.trim() === '') {
            return;
        }
        const normalizedBucket = this.normalizeBucket(bucket);
        const key = this.getProactiveRenewalKey(providerName, normalizedBucket);
        const nowSec = Date.now() / 1000;
        const remainingSec = token.expiry - nowSec;
        // @plan PLAN-20260223-ISSUE1598.P14
        // @requirement REQ-1598-PR01
        // Fix: Don't schedule proactive renewal for expired or short-lived tokens
        // Clear any stale timer so a prior schedule doesn't fire unexpectedly
        if (remainingSec < 300) {
            this.clearProactiveRenewal(key);
            return;
        }
        const leadSec = Math.max(300, Math.floor(remainingSec * 0.1));
        const jitterSec = Math.floor(Math.random() * 30);
        const refreshAtSec = token.expiry - leadSec - jitterSec;
        const delayMs = Math.floor(Math.max(0, (refreshAtSec - nowSec) * 1000));
        const existing = this.proactiveRenewals.get(key);
        if (existing && existing.expiry === token.expiry) {
            return;
        }
        this.proactiveRenewalFailures.delete(key);
        this.proactiveRenewalTokens.set(key, token.access_token);
        this.setProactiveTimer(providerName, normalizedBucket, delayMs, token.expiry);
    }
    /**
     * @plan PLAN-20260223-ISSUE1598.P14
     * @requirement REQ-1598-PR02, REQ-1598-PR03, REQ-1598-PR04
     * @pseudocode proactive-renewal.md lines 51-91
     */
    async runProactiveRenewal(providerName, bucket) {
        const normalizedBucket = this.normalizeBucket(bucket);
        const key = this.getProactiveRenewalKey(providerName, normalizedBucket);
        if (this.proactiveRenewalInFlight.has(key)) {
            return;
        }
        this.proactiveRenewalInFlight.add(key);
        try {
            if (!this.isOAuthEnabled(providerName)) {
                this.clearProactiveRenewal(key);
                return;
            }
            const provider = this.providers.get(providerName);
            if (!provider) {
                // Provider might not be registered in this runtime; keep the timer but back off.
                this.scheduleProactiveRetry(providerName, normalizedBucket);
                return;
            }
            // Issue #1159: Acquire lock before refreshing
            const lockAcquired = await this.tokenStore.acquireRefreshLock(providerName, { waitMs: 10000, staleMs: 30000, bucket: normalizedBucket });
            if (!lockAcquired) {
                // Lock timeout - retry later
                this.scheduleProactiveRetry(providerName, normalizedBucket);
                return;
            }
            try {
                // Issue #1159: Double-check pattern - re-read token after acquiring lock
                const currentToken = await this.tokenStore.getToken(providerName, normalizedBucket);
                if (!currentToken || !currentToken.refresh_token) {
                    this.clearProactiveRenewal(key);
                    return;
                }
                // @plan PLAN-20260223-ISSUE1598.P14
                // @requirement REQ-1598-PR02
                // Issue #1159: Double-check - skip refresh if another process already updated
                // the token while we waited for the lock. Two detection methods:
                // 1. If we have the scheduled token's access_token, compare directly
                // 2. Otherwise fall back to expiry check (token well beyond 30s validity)
                const scheduledAccessToken = this.proactiveRenewalTokens.get(key);
                if (scheduledAccessToken) {
                    if (currentToken.access_token !== scheduledAccessToken) {
                        // Token was refreshed by another process — reschedule with the fresh token
                        this.scheduleProactiveRenewal(providerName, normalizedBucket, currentToken);
                        return;
                    }
                }
                else {
                    // Direct runProactiveRenewal call (no prior schedule) — use expiry-based check
                    const nowInSeconds = Math.floor(Date.now() / 1000);
                    if (currentToken.expiry > nowInSeconds + 30) {
                        // Token is still well-valid — another process likely refreshed it
                        this.scheduleProactiveRenewal(providerName, normalizedBucket, currentToken);
                        return;
                    }
                }
                const refreshedToken = await provider.refreshToken(currentToken);
                if (!refreshedToken) {
                    // @plan PLAN-20260223-ISSUE1598.P14
                    // @requirement REQ-1598-PR04, REQ-1598-PR05
                    this.scheduleProactiveRetry(providerName, normalizedBucket);
                    return;
                }
                const mergedToken = mergeRefreshedToken(currentToken, refreshedToken);
                await this.tokenStore.saveToken(providerName, mergedToken, normalizedBucket);
                // @plan PLAN-20260223-ISSUE1598.P14
                // @requirement REQ-1598-PR03
                this.proactiveRenewalFailures.delete(key);
                this.scheduleProactiveRenewal(providerName, normalizedBucket, mergedToken);
            }
            finally {
                // Always release lock
                await this.tokenStore.releaseRefreshLock(providerName, normalizedBucket);
            }
        }
        finally {
            this.proactiveRenewalInFlight.delete(key);
        }
    }
    async configureProactiveRenewalsForProfile(profile) {
        const desiredKeys = new Set();
        const targets = [];
        const direct = getOAuthBucketsFromProfile(profile);
        if (direct) {
            for (const bucket of direct.buckets) {
                targets.push({ providerName: direct.providerName, bucket });
            }
        }
        if (isLoadBalancerProfileLike(profile)) {
            const profileManager = await createProfileManager();
            const visited = new Set();
            const visit = async (profileName) => {
                if (visited.has(profileName)) {
                    return;
                }
                visited.add(profileName);
                let loaded;
                try {
                    loaded = await profileManager.loadProfile(profileName);
                }
                catch (error) {
                    logger.debug(() => `[OAUTH] Failed to load profile '${profileName}' for proactive renewals: ${error instanceof Error ? error.message : String(error)}`);
                    return;
                }
                const oauth = getOAuthBucketsFromProfile(loaded);
                if (oauth) {
                    for (const bucket of oauth.buckets) {
                        targets.push({ providerName: oauth.providerName, bucket });
                    }
                }
                if (isLoadBalancerProfileLike(loaded)) {
                    for (const child of loaded.profiles) {
                        await visit(child);
                    }
                }
            };
            for (const name of profile.profiles) {
                await visit(name);
            }
        }
        for (const target of targets) {
            const bucket = this.normalizeBucket(target.bucket);
            desiredKeys.add(this.getProactiveRenewalKey(target.providerName, bucket));
        }
        for (const existingKey of Array.from(this.proactiveRenewals.keys())) {
            if (!desiredKeys.has(existingKey)) {
                this.clearProactiveRenewal(existingKey);
            }
        }
        for (const target of targets) {
            if (!this.isOAuthEnabled(target.providerName)) {
                continue;
            }
            const bucket = this.normalizeBucket(target.bucket);
            const token = await this.tokenStore.getToken(target.providerName, bucket);
            if (!token) {
                continue;
            }
            this.scheduleProactiveRenewal(target.providerName, bucket, token);
        }
    }
    /**
     * 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();
            }
            // Flush all known runtime scopes to ensure cached tokens are invalidated
            // This fixes Issue #975 where logout didn't invalidate in-memory cached tokens
            const knownRuntimeIds = [
                'legacy-singleton',
                'provider-manager-singleton',
            ];
            try {
                const runtimeContext = getCliRuntimeContext();
                if (runtimeContext && typeof runtimeContext.runtimeId === 'string') {
                    if (!knownRuntimeIds.includes(runtimeContext.runtimeId)) {
                        knownRuntimeIds.push(runtimeContext.runtimeId);
                    }
                }
            }
            catch (runtimeError) {
                logger.debug(`Could not get CLI runtime context for ${providerName}:`, runtimeError);
            }
            // Flush all known runtime scopes
            for (const runtimeId of knownRuntimeIds) {
                try {
                    flushRuntimeAuthScope(runtimeId);
                    logger.debug(`Flushed runtime auth scope: ${runtimeId}`);
                }
                catch (flushError) {
                    logger.debug(`Skipped flush for runtime ${runtimeId}:`, flushError);
                }
            }
            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)
     */
    /**
     * Get Anthropic usage information from OAuth endpoint for a specific bucket
     * Returns full usage data for Claude Code/Max plans
     * Only works with OAuth tokens (sk-ant-oat01-...), not API keys
     * @param bucket - Optional bucket name, defaults to current session bucket or 'default'
     */
    async getAnthropicUsageInfo(bucket) {
        const provider = this.providers.get('anthropic');
        if (!provider) {
            return null;
        }
        // Get the token for the specified bucket
        const bucketToUse = bucket ?? this.sessionBuckets.get('anthropic') ?? 'default';
        const token = await this.tokenStore.getToken('anthropic', bucketToUse);
        if (!token) {
            return null;
        }
        try {
            const { fetchAnthropicUsage } = await import('@vybestack/llxprt-code-core');
            return await fetchAnthropicUsage(token.access_token);
        }
        catch (error) {
            logger.debug(`Error fetching Anthropic usage info for bucket ${bucketToUse}:`, error);
            return null;
        }
    }
    /**
     * Get Anthropic usage information for all authenticated buckets
     * Returns a map of bucket name to usage info for all buckets that have valid OAuth tokens
     */
    async getAllAnthropicUsageInfo() {
        const result = new Map();
        // Get all buckets for anthropic
        const buckets = await this.tokenStore.listBuckets('anthropic');
        // If no buckets, try 'default'
        const bucketsToCheck = buckets.length > 0 ? buckets : ['default'];
        // Import once before the loop
        const { fetchAnthropicUsage } = await import('@vybestack/llxprt-code-core');
        for (const bucket of bucketsToCheck) {
            // Check if this bucket has a valid OAuth token
            const token = await this.tokenStore.getToken('anthropic', bucket);
            if (!token) {
                continue;
            }
            // Check if token is expired
            const nowInSeconds = Math.floor(Date.now() / 1000);
            if (token.expiry <= nowInSeconds) {
                continue;
            }
            // Check if it's an OAuth token (sk-ant-oat01-...)
            if (!token.access_token.startsWith('sk-ant-oat01-')) {
                continue;
            }
            try {
                const usageInfo = await fetchAnthropicUsage(token.access_token);
                if (usageInfo) {
                    result.set(bucket, usageInfo);
                }
            }
            catch (error) {
                logger.debug(`Error fetching Anthropic usage info for bucket ${bucket}:`, error);
            }
        }
        return result;
    }
    /**
     * Get Codex usage information for all authenticated buckets
     * Returns a map of bucket name to usage info for all buckets that have valid OAuth tokens with account_id
     */
    async getAllCodexUsageInfo() {
        const result = new Map();
        // Get all buckets for codex
        const buckets = await this.tokenStore.listBuckets('codex');
        // If no buckets, try 'default'
        const bucketsToCheck = buckets.length > 0 ? buckets : ['default'];
        // Import once before the loop
        const { fetchCodexUsage } = await import('@vybestack/llxprt-code-core');
        for (const bucket of bucketsToCheck) {
            // Check if this bucket has a valid OAuth token
            const token = await this.tokenStore.getToken('codex', bucket);
            if (!token) {
                continue;
            }
            // Check if token is expired
            const nowInSeconds = Math.floor(Date.now() / 1000);
            if (token.expiry <= nowInSeconds) {
                continue;
            }
            // Extract account_id from token (Codex tokens have this field)
            // Use runtime property access without narrowing type assertion
            const tokenObj = token;
            const accountId = typeof tokenObj['account_id'] === 'string'
                ? tokenObj['account_id']
                : undefined;
            if (!accountId) {
                logger.debug(`Codex token for bucket ${bucket} does not have account_id, skipping`);
                continue;
            }
            // Fetch usage info for this bucket
            try {
                const config = this.getConfig?.();
                const runtimeBaseUrl = config?.getEphemeralSetting('base-url');
                const codexBaseUrl = typeof runtimeBaseUrl === 'string' && runtimeBaseUrl.trim() !== ''
                    ? runtimeBaseUrl
                    : undefined;
                const usageInfo = await fetchCodexUsage(token.access_token, accountId, codexBaseUrl);
                if (usageInfo) {
                    result.set(bucket, usageInfo);
                }
            }
            catch (error) {
                logger.debug(`Error fetching Codex usage info for bucket ${bucket}:`, error);
            }
        }
        return result;
    }
    /**
     * Get Gemini quota information for all authenticated buckets.
     * Uses the CodeAssist retrieveUserQuota API via direct HTTP calls.
     * Returns a map of bucket name to quota response.
     */
    async getAllGeminiUsageInfo() {
        const result = new Map();
        // Get all buckets for gemini
        const buckets = await this.tokenStore.listBuckets('gemini');
        // If no buckets, try 'default'
        const bucketsToCheck = buckets.length > 0 ? buckets : ['default'];
        const { fetchGeminiQuota } = await import('@vybestack/llxprt-code-core');
        for (const bucket of bucketsToCheck) {
            const token = await this.tokenStore.getToken('gemini', bucket);
            if (!token) {
                continue;
            }
            // Check if token is expired
            const nowInSeconds = Math.floor(Date.now() / 1000);
            if (token.expiry <= nowInSeconds) {
                continue;
            }
            try {
                const quotaInfo = await fetchGeminiQuota(token.access_token);
                if (quotaInfo) {
                    result.set(bucket, quotaInfo);
                }
            }
            catch (error) {
                logger.debug(`Error fetching Gemini quota for bucket ${bucket}:`, error);
            }
        }
        return result;
    }
    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 createProfileManager();
            const profile = await profileManager.loadProfile(currentProfileName);
            // Issue #1468: Verify the profile's provider matches the requested provider
            // Without this check, buckets from one provider's profile could be returned
            // when requesting buckets for a different provider, causing token storage
            // corruption (e.g., Anthropic tokens saved under codex:bucket keys)
            const profileProvider = 'provider' in profile && typeof profile.provider === 'string'
                ? profile.provider
                : null;
            if (profileProvider !== providerName) {
                logger.debug(`Profile provider '${profileProvider}' does not match requested provider '${providerName}', returning empty buckets`);
                return [];
            }
            // 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
     *
     * Issue 913: This method now supports eager authentication of all buckets upfront,
     * filtering out already-authenticated buckets to avoid unnecessary prompts.
     */
    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,
        });
        // Issue 913 FIX: Filter out already-authenticated buckets for eager auth
        // Only prompt/authenticate buckets that don't have valid tokens
        const unauthenticatedBuckets = [];
        const nowInSeconds = Math.floor(Date.now() / 1000);
        for (const bucket of buckets) {
            const existingToken = await this.tokenStore.getToken(providerName, bucket);
            // Check if token exists and is not expired (with 30-second buffer)
            if (existingToken && existingToken.expiry > nowInSeconds + 30) {
                logger.debug(`Bucket ${bucket} already authenticated, skipping`, {
                    provider: providerName,
                    bucket,
                    expiry: existingToken.expiry,
                });
            }
            else {
                unauthenticatedBuckets.push(bucket);
            }
        }
        // If all buckets are already authenticated, nothing to do
        if (unauthenticatedBuckets.length === 0) {
            logger.debug('All buckets already authenticated', {
                provider: providerName,
                bucketCount: buckets.length,
            });
            return;
        }
        logger.debug('Buckets requiring authentication', {
            provider: providerName,
            total: buckets.length,
            needsAuth: unauthenticatedBuckets.length,
            buckets: unauthenticatedBuckets,
        });
        // Callback to authenticate a single bucket
        const onAuthBucket = async (provider, bucket, index, total) => {
            // Defense-in-depth: re-check token right before auth (TOCTOU fix, issue 1652)
            // Primary protection is the auth lock in authenticate(), but this catches
            // cross-process tokens written between upfront check and onAuthBucket execution
            try {
                const existingToken = await this.tokenStore.getToken(provider, bucket);
                const now = Math.floor(Date.now() / 1000);
                if (existingToken && existingToken.expiry > now + 30) {
                    logger.debug(`Bucket ${bucket} already authenticated (cross-process), skipping`);
                    return;
                }
            }
            catch (peekError) {
                logger.debug(`TOCTOU peek failed for ${provider}/${bucket}, proceeding with auth:`, peekError);
            }
            // 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) => {
            // If user already dismissed in this session, skip the dialog and proceed directly
            if (this.userDismissedAuthPrompt) {
                logger.debug('User previously dismissed auth prompt in this session, proceeding directly', { provider, bucket });
                return true;
            }
            // Check if prompt mode is enabled FIRST - this determines timeout behavior
            // Issue 913: When prompt mode is enabled, wait indefinitely for user approval
            const showPrompt = getEphemeralSetting('auth-bucket-prompt');
            // 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,
                        promptMode: showPrompt,
                    });
                    // Request confirmation via message bus
                    const confirmPromise = messageBus.requestBucketAuthConfirmation(provider, bucket, buckets.indexOf(bucket) + 1, buckets.length);
                    // Issue 913 FIX: When prompt mode is enabled, wait indefinitely for user approval
                    // The MessageBus has its own 5-minute timeout as an emergency safeguard
                    if (showPrompt) {
                        logger.debug('Prompt mode enabled - waiting indefinitely for user approval');
                        const result = await confirmPromise;
                        logger.debug('User responded to bucket auth confirmation', {
                            result,
                        });
                        if (!result) {
                            this.userDismissedAuthPrompt = true;
                        }
                        return result;
                    }
                    // Prompt mode disabled: Race with a 3-second timeout for backward compatibility
                    // If TUI is ready, it will respond; otherwise we fall back to delay
                    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,
                        });
                        if (!result) {
                            this.userDismissedAuthPrompt = true;
                        }
                        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
            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');
                const stdinWasPaused = process.stdin.isPaused();
                const stdinHadRawMode = process.stdin.isTTY &&
                    typeof process.stdin.isRaw === 'boolean' &&
                    process.stdin.isRaw;
                let rawModeChanged = false;
                let stdinStateRestored = false;
                const restoreStdinState = () => {
                    if (stdinStateRestored) {
                        return;
                    }
                    stdinStateRestored = true;
                    if (rawModeChanged && process.stdin.isTTY) {
                        try {
                            process.stdin.setRawMode(stdinHadRawMode);
                        }
                        catch {
                            // Issue #1020: Ignore EIO errors during cleanup
                        }
                    }
                    if (stdinWasPaused) {
                        try {
                            process.stdin.pause();
                        }
                        catch {
                            // Ignore pause cleanup errors
                        }
                    }
                };
                try {
                    await new Promise((resolve, reject) => {
                        const cleanup = () => {
                            process.stdin.removeListener('data', onData);
                            process.stdin.removeListener('error', onError);
                            restoreStdinState();
                        };
                        const onData = () => {
                            cleanup();
                            resolve();
                        };
                        // Issue #1020: Make error handler defensive against EIO errors
                        const onError = (err) => {
                            cleanup();
                            // Check if this is a transient I/O error we should ignore
                            const nodeError = err;
                            const isEioError = nodeError.code === 'EIO' ||
                                nodeError.errno === -5 ||
                                (typeof nodeError.message === 'string' &&
                                    nodeError.message.includes('EIO'));
                            if (isEioError) {
                                // EIO errors are transient - treat as user cancel instead of crashing
                                logger.debug('Ignoring transient stdin EIO error during prompt');
                                reject(new Error('Prompt cancelled due to I/O error'));
                            }
                            else {
                                reject(err);
                            }
                        };
                        if (!stdinHadRawMode && process.stdin.isTTY) {
                            try {
                                // Issue #1020: Wrap setRawMode in try-catch
                                process.stdin.setRawMode(true);
                                rawModeChanged = true;
                            }
                            catch (err) {
                                // If setRawMode fails, EIO-style errors should not crash
                                logger.debug('Failed to set raw mode for prompt:', err);
                                cleanup();
                                reject(new Error('Failed to set raw mode for prompt'));
                                return; // Don't continue setting up listeners
                            }
                        }
                        if (stdinWasPaused) {
                            process.stdin.resume();
                        }
                        process.stdin.once('data', onData);
                        process.stdin.once('error', onError);
                    });
                }
                catch (error) {
                    restoreStdinState();
                    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,
        });
        // Issue 913: Use unauthenticatedBuckets for the actual auth flow
        const result = await authenticator.authenticateMultipleBuckets({
            provider: providerName,
            buckets: unauthenticatedBuckets,
        });
        if (result.cancelled) {
            throw new Error(`Multi-bucket authentication cancelled after ${result.authenticatedBuckets.length} of ${unauthenticatedBuckets.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