/**
 * @license
 * Copyright 2025 Vybestack LLC
 * SPDX-License-Identifier: Apache-2.0
 * @plan PLAN-20250909-TOKTRACK.P08
 */
import { LoggingProviderWrapper } from './LoggingProviderWrapper.js';
import { logProviderSwitch, logProviderCapability, } from '../telemetry/loggers.js';
import { ProviderSwitchEvent, ProviderCapabilityEvent, } from '../telemetry/types.js';
import { getActiveProviderRuntimeContext, } from '../runtime/providerRuntimeContext.js';
import { MissingProviderRuntimeError, ProviderRuntimeNormalizationError, } from './errors.js';
import { createRuntimeInvocationContext } from '../runtime/RuntimeInvocationContext.js';
import { DebugLogger } from '../debug/DebugLogger.js';
const PROVIDER_CAPABILITY_HINTS = {
    gemini: {
        hasModelSelection: true,
        hasApiKeyConfig: true,
        hasBaseUrlConfig: false,
    },
    openai: {
        hasModelSelection: true,
        hasApiKeyConfig: true,
        hasBaseUrlConfig: true,
    },
    'openai-responses': {
        hasModelSelection: false,
        hasApiKeyConfig: true,
        hasBaseUrlConfig: true,
    },
    anthropic: {
        hasModelSelection: true,
        hasApiKeyConfig: true,
        hasBaseUrlConfig: true,
    },
    openaivercel: {
        hasModelSelection: true,
        hasApiKeyConfig: true,
        hasBaseUrlConfig: true,
    },
};
const logger = new DebugLogger('llxprt:provider:manager');
export class ProviderManager {
    providers;
    serverToolsProvider;
    config;
    /**
     * @plan PLAN-20250218-STATELESSPROVIDER.P05
     * @requirement REQ-SP-001
     * @pseudocode provider-invocation.md lines 8-15
     */
    settingsService;
    runtime;
    providerCapabilities = new Map();
    sessionTokenUsage = {
        input: 0,
        output: 0,
        cache: 0,
        tool: 0,
        thought: 0,
        total: 0,
    };
    cacheStats = {
        totalCacheReads: 0,
        totalCacheWrites: null,
        requestsWithCacheHits: 0,
        requestsWithCacheWrites: 0,
        hitRate: 0,
    };
    constructor(init) {
        const resolved = this.resolveInit(init);
        this.providers = new Map();
        this.serverToolsProvider = null;
        this.settingsService = resolved.settingsService;
        this.config = resolved.config ?? this.config;
        this.runtime = resolved.runtime;
    }
    /**
     * @plan PLAN-20250218-STATELESSPROVIDER.P05
     * @requirement REQ-SP-001
     * @pseudocode provider-invocation.md lines 8-15
     * @plan:PLAN-20251018-STATELESSPROVIDER2.P03
     * @requirement:REQ-SP2-002
     * @pseudocode multi-runtime-baseline.md lines 3-4
     */
    resolveInit(init) {
        let fallback = null;
        const ensureFallback = () => {
            if (!fallback) {
                fallback = getActiveProviderRuntimeContext();
            }
            return fallback;
        };
        if (!init) {
            const resolved = ensureFallback();
            return {
                settingsService: resolved.settingsService,
                config: resolved.config,
                runtime: resolved,
            };
        }
        if (typeof init === 'object' &&
            init !== null &&
            'settingsService' in init &&
            ('runtimeId' in init || 'metadata' in init)) {
            const context = init;
            return {
                settingsService: context.settingsService,
                config: context.config,
                runtime: context,
            };
        }
        const initObj = init;
        const runtime = initObj.runtime;
        let settingsService = initObj.settingsService ?? runtime?.settingsService ?? null;
        let config = initObj.config ?? runtime?.config ?? undefined;
        if (!settingsService || !config) {
            const resolved = ensureFallback();
            settingsService = settingsService ?? resolved.settingsService;
            config = config ?? resolved.config;
            return {
                settingsService,
                config,
                runtime: runtime ?? resolved,
            };
        }
        return {
            settingsService,
            config,
            runtime,
        };
    }
    setConfig(config) {
        const hadConfig = Boolean(this.config);
        const oldLoggingEnabled = this.config?.getConversationLoggingEnabled() ?? false;
        const newLoggingEnabled = config.getConversationLoggingEnabled();
        this.config = config;
        this.runtime = this.runtime
            ? { ...this.runtime, config }
            : { settingsService: this.settingsService, config };
        // Always ensure providers are wrapped once config becomes available
        if (!hadConfig || oldLoggingEnabled !== newLoggingEnabled) {
            logger.debug(() => `[provider-manager] Wrapping providers (hadConfig=${hadConfig}, loggingChanged=${oldLoggingEnabled !== newLoggingEnabled})`);
            this.updateProviderWrapping();
        }
    }
    /**
     * @plan PLAN-20251018-STATELESSPROVIDER2.P06
     * @requirement REQ-SP2-001
     * @pseudocode base-provider-call-contract.md lines 3-5
     */
    updateProviderWrapping() {
        logger.debug(() => `[provider-manager] updateProviderWrapping invoked (providerCount=${this.providers.size}, hasConfig=${Boolean(this.config)})`);
        // Re-wrap all providers (ALWAYS wrap for token tracking)
        const providers = new Map(this.providers);
        for (const [name, provider] of providers) {
            // Unwrap if it's already wrapped
            let baseProvider = provider;
            if ('wrappedProvider' in provider && provider.wrappedProvider) {
                baseProvider = provider.wrappedProvider;
            }
            this.syncProviderRuntime(baseProvider);
            // ALWAYS wrap with LoggingProviderWrapper for token tracking
            let finalProvider = baseProvider;
            if (this.config) {
                finalProvider = new LoggingProviderWrapper(baseProvider, this.config);
            }
            this.syncProviderRuntime(finalProvider);
            this.providers.set(name, finalProvider);
            // Update server tools provider reference if needed
            if (this.serverToolsProvider && this.serverToolsProvider.name === name) {
                this.serverToolsProvider = finalProvider;
            }
        }
    }
    /**
     * @plan PLAN-20250218-STATELESSPROVIDER.P05
     * @requirement REQ-SP-001
     * @pseudocode provider-invocation.md lines 8-15
     * @plan:PLAN-20251023-STATELESS-HARDENING.P05
     * @requirement:REQ-SP4-001
     * @pseudocode provider-runtime-handling.md lines 10-15
     */
    syncProviderRuntime(provider) {
        const runtimeAware = provider;
        runtimeAware.setRuntimeSettingsService?.(this.settingsService);
        if (this.config && runtimeAware.setConfig) {
            runtimeAware.setConfig(this.config);
        }
        if (runtimeAware.setRuntimeContextResolver &&
            typeof runtimeAware.setRuntimeContextResolver === 'function') {
            runtimeAware.setRuntimeContextResolver(() => this.snapshotRuntimeContext('ProviderManager.syncProviderRuntime'));
        }
        if (runtimeAware.setOptionsNormalizer &&
            typeof runtimeAware.setOptionsNormalizer === 'function') {
            runtimeAware.setOptionsNormalizer((options, providerName) => this.normalizeRuntimeInputs(options, providerName));
        }
    }
    /**
     * @plan:PLAN-20251023-STATELESS-HARDENING.P05
     * @requirement:REQ-SP4-001
     * @pseudocode provider-runtime-handling.md lines 10-15
     */
    snapshotRuntimeContext(source) {
        const baseRuntime = this.runtime ?? {
            settingsService: this.settingsService,
            config: this.config,
            runtimeId: 'provider-manager.default-runtime',
            metadata: { source: 'ProviderManager', requirement: 'REQ-SP4-001' },
        };
        if (!this.runtime) {
            this.runtime = baseRuntime;
        }
        else if (!this.runtime.config && baseRuntime.config) {
            this.runtime = { ...this.runtime, config: baseRuntime.config };
        }
        const settingsService = baseRuntime.settingsService;
        if (!settingsService) {
            throw new MissingProviderRuntimeError({
                providerKey: 'ProviderManager',
                missingFields: ['settings'],
                stage: source,
                metadata: {
                    requirement: 'REQ-SP4-001',
                    hint: 'ProviderManager requires a SettingsService to construct runtime contexts.',
                },
            });
        }
        const config = baseRuntime.config ?? this.config;
        if (!config) {
            throw new MissingProviderRuntimeError({
                providerKey: 'ProviderManager',
                missingFields: ['config'],
                stage: source,
                metadata: {
                    requirement: 'REQ-SP4-001',
                    hint: 'Call ProviderManager.setConfig before invoking providers.',
                },
            });
        }
        const baseMetadata = baseRuntime.metadata ?? {};
        const callMetadata = {
            ...baseMetadata,
            source,
            requirement: 'REQ-SP4-001',
            generatedAt: new Date().toISOString(),
        };
        const baseId = baseRuntime.runtimeId;
        const callRuntimeId = typeof baseId === 'string' && baseId.trim() !== ''
            ? `${baseId}:${Math.random().toString(36).slice(2, 10)}`
            : `provider-manager:${source}:${Date.now().toString(36)}`;
        return {
            settingsService,
            config,
            runtimeId: callRuntimeId,
            metadata: callMetadata,
        };
    }
    /**
     * @plan:PLAN-20251023-STATELESS-HARDENING.P08
     * @requirement:REQ-SP4-002
     * @requirement:REQ-SP4-003
     * @requirement:REQ-SP4-004
     * @requirement:REQ-SP4-005
     * @pseudocode provider-runtime-handling.md lines 10-16
     *
     * Normalize runtime inputs per call - no stored settings/config fallbacks.
     * This method enforces that all runtime context is provided per-call and that
     * providers cannot rely on stored state.
     */
    normalizeRuntimeInputs(rawOptions, providerName) {
        const runtimeId = rawOptions.runtime?.runtimeId || 'unknown';
        const targetProvider = providerName || this.getActiveProviderName();
        // REQ-SP4-002: Check for required settings service and config in runtime context
        const settingsService = rawOptions.settings ?? rawOptions.runtime?.settingsService;
        const config = rawOptions.config ?? rawOptions.runtime?.config;
        if (!settingsService) {
            throw new ProviderRuntimeNormalizationError({
                providerKey: 'ProviderManager',
                message: 'ProviderManager requires call-scoped settings; legacy provider state is disabled.',
                requirement: 'REQ-SP4-002',
                runtimeId,
                stage: 'normalizeRuntimeInputs',
                metadata: {
                    hint: 'SettingsService must be provided in options.settings or runtime.settingsService',
                },
            });
        }
        if (!config) {
            throw new ProviderRuntimeNormalizationError({
                providerKey: 'ProviderManager',
                message: 'ProviderManager requires call-scoped config; legacy provider state is disabled.',
                requirement: 'REQ-SP4-002',
                runtimeId,
                stage: 'normalizeRuntimeInputs',
                metadata: {
                    hint: 'Config must be provided in options.config or runtime.config',
                },
            });
        }
        // REQ-SP4-003: Compose normalized.resolved with runtime helpers
        const providerSettings = settingsService.getProviderSettings(targetProvider);
        const providerInstance = this.providers.get(targetProvider);
        const configSettingsService = typeof config
            .getSettingsService === 'function'
            ? config.getSettingsService()
            : undefined;
        const configMatchesSettingsService = !configSettingsService || configSettingsService === settingsService;
        const activeProviderRaw = settingsService.get('activeProvider');
        const activeProviderName = typeof activeProviderRaw === 'string' ? activeProviderRaw.trim() : '';
        const shouldApplyGlobalEphemerals = configMatchesSettingsService &&
            (!activeProviderName || activeProviderName === targetProvider);
        // Debug: Log incoming authToken before normalization
        logger.debug(() => {
            const token = rawOptions.resolved?.authToken;
            const tokenStr = typeof token === 'string' ? token : '';
            return `[normalizeRuntimeInputs] provider=${targetProvider}, incoming authToken present=${Boolean(tokenStr.trim())} length=${tokenStr.length}`;
        });
        const resolved = {
            model: rawOptions.resolved?.model ??
                providerSettings.model ??
                (shouldApplyGlobalEphemerals ? config.getModel?.() : undefined) ??
                providerInstance?.getDefaultModel?.() ??
                undefined,
            baseURL: rawOptions.resolved?.baseURL ??
                providerSettings.baseURL ??
                providerSettings.baseUrl,
            authToken: rawOptions.resolved?.authToken ??
                providerSettings.apiKey,
            telemetry: {
                ...rawOptions.resolved?.telemetry,
                runtimeId,
                normalizedAt: new Date().toISOString(),
                provider: targetProvider,
            },
        };
        const effectiveConfig = rawOptions.config ?? config ?? null;
        // Debug: Log resolved authToken before global auth-key check
        logger.debug(() => {
            const token = resolved.authToken;
            const tokenStr = typeof token === 'string' ? token : '';
            return `[normalizeRuntimeInputs] provider=${targetProvider}, resolved authToken present=${Boolean(tokenStr.trim())} length=${tokenStr.length}`;
        });
        if (shouldApplyGlobalEphemerals &&
            effectiveConfig &&
            typeof effectiveConfig.getEphemeralSetting === 'function' &&
            (typeof resolved.authToken !== 'string' ||
                resolved.authToken.trim() === '')) {
            const globalAuthKey = effectiveConfig.getEphemeralSetting?.('auth-key');
            // Debug: Log global auth-key check
            logger.debug(() => {
                const tokenStr = typeof globalAuthKey === 'string' ? globalAuthKey : '';
                return `[normalizeRuntimeInputs] provider=${targetProvider}, global auth-key present=${Boolean(tokenStr.trim())} length=${tokenStr.length}, will use: ${globalAuthKey ? 'YES' : 'NO'}`;
            });
            if (globalAuthKey && globalAuthKey.trim() !== '') {
                resolved.authToken = globalAuthKey.trim();
            }
            else if (process.env.DEBUG) {
                logger.debug(() => `[ProviderManager] Missing auth token for provider '${targetProvider}' even after checking global auth-key.`);
            }
        }
        if (!resolved.baseURL && shouldApplyGlobalEphemerals) {
            const configBaseUrl = typeof config.getEphemeralSetting === 'function'
                ? config.getEphemeralSetting('base-url')
                : undefined;
            if (configBaseUrl && typeof configBaseUrl === 'string') {
                const trimmed = configBaseUrl.trim();
                if (trimmed) {
                    resolved.baseURL = trimmed;
                }
            }
        }
        if (!resolved.baseURL) {
            const providerBaseUrl = this.getBaseUrlFromProvider(providerInstance);
            if (providerBaseUrl) {
                resolved.baseURL = providerBaseUrl;
            }
        }
        // REQ-SP4-003: Validate required fields in resolved options
        const missingFields = [];
        if (!resolved.model)
            missingFields.push('model');
        // Note: Gemini and some providers don't require baseURL/authToken in all configurations
        const baseUrlOptionalProviders = new Set([
            'gemini',
            'openai',
            'openai-responses',
            'anthropic',
            'openaivercel',
            'load-balancer', // Resolves baseURL at request time via sub-profile selection
        ]);
        if (!resolved.baseURL && !baseUrlOptionalProviders.has(targetProvider)) {
            missingFields.push('baseURL');
        }
        if (!resolved.authToken && targetProvider !== 'gemini') {
            // Check if provider can resolve auth lazily (e.g., via OAuth)
            // If the provider has getAuthToken(), it can handle its own auth precedence
            const providerInstance = this.providers.get(targetProvider);
            let actualProvider = providerInstance;
            if (providerInstance && 'wrappedProvider' in providerInstance) {
                actualProvider = providerInstance
                    .wrappedProvider;
            }
            const canResolveAuth = actualProvider &&
                'getAuthToken' in actualProvider &&
                typeof actualProvider.getAuthToken === 'function';
            if (!canResolveAuth) {
                // Only fail for providers without lazy auth resolution capability
                missingFields.push('authToken');
            }
            // Otherwise let the provider run its multi-modal precedence chain:
            // 1. Manual key (from /key)
            // 2. Keyfile (from /keyfile)
            // 3. Environment variables
            // 4. OAuth token (if enabled)
        }
        if (missingFields.length > 0) {
            throw new ProviderRuntimeNormalizationError({
                providerKey: 'ProviderManager',
                message: `Incomplete runtime resolution (${missingFields.join(', ')}) for runtimeId=${runtimeId}`,
                requirement: 'REQ-SP4-003',
                runtimeId,
                stage: 'normalizeRuntimeInputs',
                metadata: { missingFields, provider: targetProvider },
            });
        }
        // REQ-SP4-005: Ensure normalized.userMemory and metadata derive from runtime context
        const userMemory = rawOptions.userMemory ?? config.getUserMemory?.();
        const metadata = {
            ...rawOptions.metadata,
            ...rawOptions.runtime?.metadata,
            _normalized: true,
            _normalizationTime: new Date().toISOString(),
            _runtimeId: runtimeId,
            _provider: targetProvider,
        };
        const normalizedRuntime = {
            ...(rawOptions.runtime ?? {}),
            settingsService,
            config,
            runtimeId,
            metadata,
        };
        const userMemorySnapshot = typeof userMemory === 'string' ? userMemory : config.getUserMemory?.();
        const invocation = rawOptions.invocation ??
            createRuntimeInvocationContext({
                runtime: normalizedRuntime,
                settings: settingsService,
                providerName: targetProvider,
                ephemeralsSnapshot: this.buildEphemeralsSnapshot(settingsService, targetProvider),
                telemetry: resolved.telemetry,
                metadata,
                userMemory: userMemorySnapshot ?? undefined,
                fallbackRuntimeId: runtimeId,
            });
        return {
            ...rawOptions,
            settings: settingsService,
            config,
            runtime: normalizedRuntime,
            resolved,
            userMemory,
            metadata,
            invocation,
        };
    }
    buildEphemeralsSnapshot(settingsService, providerName) {
        const globalEphemerals = settingsService.getAllGlobalSettings();
        const providerEphemerals = settingsService.getProviderSettings(providerName);
        const snapshot = {
            ...globalEphemerals,
        };
        snapshot[providerName] = { ...providerEphemerals };
        return snapshot;
    }
    /**
     * @plan PLAN-20251018-STATELESSPROVIDER2.P06
     * @requirement REQ-SP2-001
     * @pseudocode base-provider-call-contract.md lines 3-5
     */
    registerProvider(provider) {
        this.syncProviderRuntime(provider);
        // ALWAYS wrap provider to enable token tracking
        // (LoggingProviderWrapper handles both token tracking AND conversation logging)
        let finalProvider = provider;
        if (this.config) {
            finalProvider = new LoggingProviderWrapper(provider, this.config);
        }
        this.syncProviderRuntime(finalProvider);
        this.providers.set(provider.name, finalProvider);
        // Capture provider capabilities
        const capabilities = this.captureProviderCapabilities(provider);
        this.providerCapabilities.set(provider.name, capabilities);
        // Log provider capability information if logging enabled
        if (this.config?.getConversationLoggingEnabled()) {
            const context = this.createProviderContext(provider, capabilities);
            logProviderCapability(this.config, new ProviderCapabilityEvent(provider.name, capabilities, context));
        }
        // If this is the default provider and no provider is active, set it as active
        const currentActiveProvider = this.settingsService.get('activeProvider');
        if (provider.isDefault && !currentActiveProvider) {
            this.settingsService.set('activeProvider', provider.name);
        }
        // If registering Gemini and we don't have a serverToolsProvider, use it
        if (provider.name === 'gemini' && !this.serverToolsProvider) {
            this.serverToolsProvider = provider;
        }
        // If Gemini is the active provider, it should also be the serverToolsProvider
        if (provider.name === 'gemini' && currentActiveProvider === 'gemini') {
            this.serverToolsProvider = provider;
        }
    }
    /**
     * @plan PLAN-20251018-STATELESSPROVIDER2.P06
     * @requirement REQ-SP2-001
     * @pseudocode base-provider-call-contract.md lines 3-5
     */
    setActiveProvider(name) {
        if (!this.providers.has(name)) {
            throw new Error('Provider not found');
        }
        // Store reference to the current active provider before switching
        const previousProviderName = this.settingsService.get('activeProvider') || '';
        // Only clear state from the provider we're switching FROM
        // BUT never clear the serverToolsProvider's state
        if (previousProviderName && previousProviderName !== name) {
            const previousProvider = this.providers.get(previousProviderName);
            if (previousProvider &&
                previousProvider !== this.serverToolsProvider &&
                'clearState' in previousProvider) {
                const candidate = previousProvider;
                candidate.clearState?.();
            }
        }
        // Log provider switch if conversation logging enabled
        if (this.config?.getConversationLoggingEnabled() &&
            previousProviderName &&
            previousProviderName !== name) {
            logProviderSwitch(this.config, new ProviderSwitchEvent(previousProviderName, name, this.generateConversationId(), this.isContextPreserved(previousProviderName, name)));
        }
        // Update SettingsService as the single source of truth
        this.settingsService.set('activeProvider', name);
        // If switching to Gemini, use it as both active and serverTools provider
        // BUT only if we don't already have a Gemini serverToolsProvider with auth state
        if (name === 'gemini') {
            // Only replace serverToolsProvider if it's not already Gemini or if it's null
            if (!this.serverToolsProvider ||
                this.serverToolsProvider.name !== 'gemini') {
                this.serverToolsProvider = this.providers.get(name) || null;
            }
        }
        // If switching away from Gemini but serverToolsProvider is not set,
        // configure a Gemini provider for serverTools if available
        else if (!this.serverToolsProvider && this.providers.has('gemini')) {
            this.serverToolsProvider = this.providers.get('gemini') || null;
        }
    }
    clearActiveProvider() {
        this.settingsService.set('activeProvider', '');
    }
    getActiveProvider() {
        const activeProviderName = this.settingsService.get('activeProvider') || '';
        let resolvedName = activeProviderName;
        if (!resolvedName) {
            const preferredFromConfig = this.config?.getProvider?.();
            if (preferredFromConfig && this.providers.has(preferredFromConfig)) {
                resolvedName = preferredFromConfig;
            }
            else if (this.providers.has('openai')) {
                resolvedName = 'openai';
            }
            else {
                const firstProvider = this.providers.keys().next();
                resolvedName = firstProvider.done ? '' : firstProvider.value;
            }
            if (resolvedName) {
                try {
                    this.setActiveProvider(resolvedName);
                }
                catch (error) {
                    throw new Error(`Unable to set default provider '${resolvedName}': ${String(error)}`);
                }
            }
        }
        if (!resolvedName) {
            throw new Error('No active provider set');
        }
        const provider = this.providers.get(resolvedName);
        if (!provider) {
            throw new Error('Active provider not found');
        }
        return provider;
    }
    async getAvailableModels(providerName) {
        let provider;
        if (providerName) {
            provider = this.providers.get(providerName);
            if (!provider) {
                throw new Error(`Provider '${providerName}' not found`);
            }
        }
        else {
            provider = this.getActiveProvider();
        }
        return provider.getModels();
    }
    listProviders() {
        const names = Array.from(this.providers.keys());
        const priorityOrder = ['anthropic', 'gemini', 'openai', 'openai-responses'];
        const prioritized = priorityOrder.filter((name) => names.includes(name));
        const remaining = names
            .filter((name) => !priorityOrder.includes(name))
            .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
        return [...prioritized, ...remaining];
    }
    /**
     * Get a provider by name (for OAuth manager)
     */
    getProviderByName(name) {
        return this.providers.get(name);
    }
    getActiveProviderName() {
        return this.settingsService.get('activeProvider') || '';
    }
    hasActiveProvider() {
        const activeProviderName = this.settingsService.get('activeProvider') || '';
        return activeProviderName !== '' && this.providers.has(activeProviderName);
    }
    getServerToolsProvider() {
        // If we have a configured serverToolsProvider, return it
        if (this.serverToolsProvider) {
            return this.serverToolsProvider;
        }
        // Otherwise, try to get Gemini if available
        const geminiProvider = this.providers.get('gemini');
        if (geminiProvider) {
            this.serverToolsProvider = geminiProvider;
            return geminiProvider;
        }
        return null;
    }
    setServerToolsProvider(provider) {
        this.serverToolsProvider = provider;
    }
    generateConversationId() {
        // Generate unique conversation ID for session
        return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }
    isContextPreserved(fromProvider, toProvider) {
        // Analyze whether context can be preserved between providers
        const fromCapabilities = this.providerCapabilities.get(fromProvider);
        const toCapabilities = this.providerCapabilities.get(toProvider);
        if (!fromCapabilities || !toCapabilities) {
            return false; // Can't analyze without capabilities
        }
        // Context is better preserved between providers with similar capabilities
        const capabilityScore = this.calculateCapabilityCompatibility(fromCapabilities, toCapabilities);
        // Context is considered preserved if compatibility is high
        return capabilityScore > 0.7;
    }
    getStoredModelName(provider) {
        const providerSettings = this.settingsService.getProviderSettings(provider.name);
        const storedModel = providerSettings.model;
        if (storedModel && typeof storedModel === 'string' && storedModel.trim()) {
            return storedModel;
        }
        if (this.config &&
            typeof this.config.getProvider === 'function' &&
            this.config.getProvider() === provider.name) {
            const configModel = this.config.getModel();
            if (configModel) {
                return configModel;
            }
        }
        return provider.getDefaultModel?.() ?? '';
    }
    captureProviderCapabilities(provider) {
        const hints = PROVIDER_CAPABILITY_HINTS[provider.name] ?? {};
        return {
            supportsStreaming: true, // All current providers support streaming
            supportsTools: provider.getServerTools().length > 0,
            supportsVision: this.detectVisionSupport(provider),
            maxTokens: this.getProviderMaxTokens(provider),
            supportedFormats: this.getSupportedToolFormats(provider),
            hasModelSelection: hints.hasModelSelection ?? true,
            hasApiKeyConfig: hints.hasApiKeyConfig ?? true,
            hasBaseUrlConfig: hints.hasBaseUrlConfig ?? true,
            supportsPaidMode: typeof provider.isPaidMode === 'function',
        };
    }
    detectVisionSupport(provider) {
        // Provider-specific vision detection logic
        const model = this.getStoredModelName(provider).toLowerCase();
        switch (provider.name) {
            case 'gemini': {
                return true;
            }
            case 'openai': {
                return model.includes('vision') || model.includes('gpt-4');
            }
            case 'anthropic': {
                return model.includes('claude-3');
            }
            default:
                return false;
        }
    }
    getProviderMaxTokens(provider) {
        const model = this.getStoredModelName(provider).toLowerCase();
        switch (provider.name) {
            case 'gemini':
                if (model.includes('pro'))
                    return 32768;
                if (model.includes('flash'))
                    return 8192;
                return 8192;
            case 'openai':
                if (model.includes('gpt-4'))
                    return 8192;
                if (model.includes('gpt-3.5'))
                    return 4096;
                return 4096;
            case 'anthropic':
                if (model.includes('claude-3'))
                    return 200000;
                return 100000;
            default:
                return 4096;
        }
    }
    getSupportedToolFormats(provider) {
        switch (provider.name) {
            case 'gemini':
                return ['function_calling', 'gemini_tools'];
            case 'openai':
                return ['function_calling', 'json_schema', 'hermes'];
            case 'anthropic':
                return ['xml_tools', 'anthropic_tools'];
            default:
                return [];
        }
    }
    createProviderContext(provider, capabilities) {
        const providerSettings = this.settingsService.getProviderSettings(provider.name);
        const toolFormatSetting = providerSettings.toolFormat ?? 'auto';
        return {
            providerName: provider.name,
            currentModel: this.getStoredModelName(provider) || 'unknown',
            toolFormat: toolFormatSetting,
            isPaidMode: provider.isPaidMode?.() || false,
            capabilities,
            sessionStartTime: Date.now(),
        };
    }
    calculateCapabilityCompatibility(from, to) {
        let score = 0;
        let totalChecks = 0;
        // Check tool support compatibility
        totalChecks++;
        if (from.supportsTools === to.supportsTools)
            score++;
        // Check vision support compatibility
        totalChecks++;
        if (from.supportsVision === to.supportsVision)
            score++;
        // Check streaming compatibility (all providers support streaming currently)
        totalChecks++;
        if (from.supportsStreaming === to.supportsStreaming)
            score++;
        // Check tool format compatibility
        totalChecks++;
        const hasCommonFormats = from.supportedFormats.some((format) => to.supportedFormats.includes(format));
        if (hasCommonFormats)
            score++;
        return score / totalChecks;
    }
    /**
     * Accumulate token usage for the current session
     */
    accumulateSessionTokens(providerName, usage) {
        logger.debug(() => `[ProviderManager.accumulateSessionTokens] Called with: cacheReads=${usage.cacheReads}, cacheWrites=${usage.cacheWrites}, cacheReads===undefined: ${usage.cacheReads === undefined}, cacheWrites===undefined: ${usage.cacheWrites === undefined}`);
        // Only accumulate non-negative values
        this.sessionTokenUsage.input += Math.max(0, usage.input || 0);
        this.sessionTokenUsage.output += Math.max(0, usage.output || 0);
        // For cache field: use the explicit cache value OR cacheReads if cache is 0
        // This handles both Gemini (which uses cached_content_token_count) and
        // Anthropic (which uses cache_read_input_tokens)
        const cacheTokens = Math.max(0, usage.cache || 0) || Math.max(0, usage.cacheReads || 0);
        this.sessionTokenUsage.cache += cacheTokens;
        this.sessionTokenUsage.tool += Math.max(0, usage.tool || 0);
        this.sessionTokenUsage.thought += Math.max(0, usage.thought || 0);
        this.sessionTokenUsage.total +=
            Math.max(0, usage.input || 0) +
                Math.max(0, usage.output || 0) +
                cacheTokens +
                Math.max(0, usage.tool || 0) +
                Math.max(0, usage.thought || 0);
        // Track cache reads/writes if provided
        // Note: cacheWrites can be null (provider doesn't report) vs undefined (not in usage object)
        if (usage.cacheReads !== undefined || usage.cacheWrites !== undefined) {
            logger.debug(() => `[ProviderManager.accumulateSessionTokens] Received cache usage: cacheReads=${usage.cacheReads}, cacheWrites=${usage.cacheWrites}`);
            this.trackCacheUsage(Math.max(0, usage.cacheReads || 0), usage.cacheWrites === null || usage.cacheWrites === undefined
                ? null
                : Math.max(0, usage.cacheWrites));
        }
        else {
            logger.debug(() => `[ProviderManager.accumulateSessionTokens] No cache usage in this request`);
        }
    }
    /**
     * Reset session token usage counters
     */
    resetSessionTokenUsage() {
        this.sessionTokenUsage = {
            input: 0,
            output: 0,
            cache: 0,
            tool: 0,
            thought: 0,
            total: 0,
        };
    }
    /**
     * Get current session token usage
     */
    getSessionTokenUsage() {
        // Validate and replace any NaN or undefined values with 0
        return {
            input: this.sessionTokenUsage.input || 0,
            output: this.sessionTokenUsage.output || 0,
            cache: this.sessionTokenUsage.cache || 0,
            tool: this.sessionTokenUsage.tool || 0,
            thought: this.sessionTokenUsage.thought || 0,
            total: this.sessionTokenUsage.total || 0,
        };
    }
    /**
     * Track cache read/write statistics from a request
     * @param cacheReads - Number of tokens read from cache
     * @param cacheWrites - Number of tokens written to cache, or null if provider doesn't report this
     */
    trackCacheUsage(cacheReads, cacheWrites) {
        logger.debug(() => `[ProviderManager.trackCacheUsage] Called with cacheReads=${cacheReads}, cacheWrites=${cacheWrites}`);
        if (cacheReads > 0) {
            this.cacheStats.totalCacheReads += cacheReads;
            this.cacheStats.requestsWithCacheHits++;
            logger.debug(() => `[ProviderManager.trackCacheUsage] Updated totalCacheReads to ${this.cacheStats.totalCacheReads}`);
        }
        // Only track cache writes if the provider reports them (not null)
        if (cacheWrites !== null) {
            // Initialize from null to 0 on first reported value
            if (this.cacheStats.totalCacheWrites === null) {
                this.cacheStats.totalCacheWrites = 0;
            }
            this.cacheStats.totalCacheWrites += cacheWrites;
            if (cacheWrites > 0) {
                this.cacheStats.requestsWithCacheWrites++;
            }
            logger.debug(() => `[ProviderManager.trackCacheUsage] Updated totalCacheWrites to ${this.cacheStats.totalCacheWrites}`);
        }
        // Recalculate hit rate
        // Hit rate = cache reads / (cache reads + uncached input) * 100
        const totalPromptTokens = this.sessionTokenUsage.input;
        const totalInputTokens = this.cacheStats.totalCacheReads + totalPromptTokens;
        if (totalInputTokens > 0) {
            this.cacheStats.hitRate =
                (this.cacheStats.totalCacheReads / totalInputTokens) * 100;
            logger.debug(() => `[ProviderManager.trackCacheUsage] Updated hitRate to ${this.cacheStats.hitRate}% (cacheReads=${this.cacheStats.totalCacheReads}, uncachedInput=${totalPromptTokens}, total=${totalInputTokens})`);
        }
    }
    /**
     * Get cache statistics
     */
    getCacheStatistics() {
        return { ...this.cacheStats };
    }
    /**
     * Get performance metrics for the active provider
     * @plan PLAN-20250909-TOKTRACK
     */
    getProviderMetrics(providerName) {
        const name = providerName || this.getActiveProvider()?.name;
        if (!name)
            return null;
        const provider = this.providers.get(name);
        if (!provider)
            return null;
        // Check if provider has getPerformanceMetrics method (LoggingProviderWrapper)
        if ('getPerformanceMetrics' in provider &&
            typeof provider.getPerformanceMetrics === 'function') {
            return provider.getPerformanceMetrics();
        }
        // Return default metrics if provider doesn't support performance tracking
        return {
            tokensPerMinute: 0,
            throttleWaitTimeMs: 0,
            totalTokens: 0,
            totalRequests: 0,
        };
    }
    resetConversationContext() {
        // Conversation ID is now managed by the logging system
    }
    getProviderCapabilities(providerName) {
        const name = providerName ||
            this.settingsService.get('activeProvider') ||
            '';
        return this.providerCapabilities.get(name);
    }
    /* @plan:PLAN-20251023-STATELESS-HARDENING.P06 */
    /* @requirement:REQ-SP4-004 */
    prepareStatelessProviderInvocation(context) {
        const stage = 'ProviderManager.prepareStatelessProviderInvocation';
        const runtimeContext = context ?? this.runtime;
        if (!runtimeContext) {
            throw new MissingProviderRuntimeError({
                providerKey: 'ProviderManager',
                missingFields: ['runtime'],
                requirement: 'REQ-SP4-004',
                stage,
                metadata: {
                    hint: 'Register CLI runtime context before invoking providers.',
                },
            });
        }
        if (!runtimeContext.settingsService) {
            throw new MissingProviderRuntimeError({
                providerKey: 'ProviderManager',
                missingFields: ['settings'],
                requirement: 'REQ-SP4-004',
                stage,
                metadata: {
                    runtimeId: runtimeContext.runtimeId,
                    hint: 'ProviderManager requires a SettingsService for stateless invocation.',
                },
            });
        }
        const resolvedConfig = runtimeContext.config ?? this.config;
        if (!resolvedConfig) {
            throw new MissingProviderRuntimeError({
                providerKey: 'ProviderManager',
                missingFields: ['config'],
                requirement: 'REQ-SP4-004',
                stage,
                metadata: {
                    runtimeId: runtimeContext.runtimeId,
                    hint: 'ProviderManager requires Config before stateless invocation.',
                },
            });
        }
        const statelessMetadata = {
            ...(runtimeContext.metadata ?? {}),
            statelessHardening: 'strict',
            statelessProviderMode: 'strict',
            statelessGuards: true,
            statelessMode: 'strict',
            requirement: 'REQ-SP4-004',
            source: stage,
            preparedAt: new Date().toISOString(),
        };
        this.runtime = {
            ...runtimeContext,
            settingsService: runtimeContext.settingsService,
            config: resolvedConfig,
            metadata: statelessMetadata,
        };
        this.settingsService = runtimeContext.settingsService;
        this.config = resolvedConfig;
        for (const provider of this.providers.values()) {
            this.attachStatelessRuntimeMetadata(provider, statelessMetadata);
        }
    }
    attachStatelessRuntimeMetadata(provider, metadata) {
        const statelessAware = provider;
        statelessAware.attachStatelessRuntimeMetadata?.(metadata);
        if (statelessAware.wrappedProvider) {
            this.attachStatelessRuntimeMetadata(statelessAware.wrappedProvider, metadata);
        }
    }
    compareProviders(provider1, provider2) {
        const cap1 = this.providerCapabilities.get(provider1);
        const cap2 = this.providerCapabilities.get(provider2);
        if (!cap1 || !cap2) {
            throw new Error('Cannot compare providers: capabilities not available');
        }
        return {
            provider1,
            provider2,
            capabilities: {
                [provider1]: cap1,
                [provider2]: cap2,
            },
            compatibility: this.calculateCapabilityCompatibility(cap1, cap2),
            recommendation: this.generateProviderRecommendation(provider1, provider2, cap1, cap2),
        };
    }
    generateProviderRecommendation(provider1, provider2, cap1, cap2) {
        if (cap1.maxTokens > cap2.maxTokens) {
            return `${provider1} supports longer contexts (${cap1.maxTokens} vs ${cap2.maxTokens} tokens)`;
        }
        if (cap1.supportsVision && !cap2.supportsVision) {
            return `${provider1} supports vision capabilities`;
        }
        if (cap1.supportedFormats.length > cap2.supportedFormats.length) {
            return `${provider1} supports more tool formats`;
        }
        return 'Providers have similar capabilities';
    }
    getBaseUrlFromProvider(provider) {
        if (!provider) {
            return undefined;
        }
        const visited = new Set();
        let current = provider;
        while (current) {
            if (visited.has(current)) {
                break;
            }
            visited.add(current);
            const baseConfig = current.baseProviderConfig;
            if (baseConfig) {
                const candidate = typeof baseConfig.baseURL === 'string' &&
                    baseConfig.baseURL.trim() !== ''
                    ? baseConfig.baseURL.trim()
                    : typeof baseConfig.baseUrl === 'string' &&
                        baseConfig.baseUrl.trim() !== ''
                        ? baseConfig.baseUrl.trim()
                        : undefined;
                if (candidate) {
                    return candidate;
                }
            }
            const providerConfig = current.providerConfig;
            if (providerConfig) {
                const candidate = typeof providerConfig.baseURL === 'string' &&
                    providerConfig.baseURL.trim() !== ''
                    ? providerConfig.baseURL.trim()
                    : typeof providerConfig.baseUrl === 'string' &&
                        providerConfig.baseUrl.trim() !== ''
                        ? providerConfig.baseUrl.trim()
                        : undefined;
                if (candidate) {
                    return candidate;
                }
            }
            const maybeHasBaseUrl = current;
            if (typeof maybeHasBaseUrl.getBaseURL === 'function') {
                try {
                    const reported = maybeHasBaseUrl.getBaseURL();
                    if (reported && reported.trim() !== '') {
                        return reported.trim();
                    }
                }
                catch {
                    // ignore provider errors
                }
            }
            const maybeWrapped = current;
            if (maybeWrapped.wrappedProvider) {
                current = maybeWrapped.wrappedProvider;
                continue;
            }
            break;
        }
        return undefined;
    }
}
//# sourceMappingURL=ProviderManager.js.map