/**
 * Provider Fallback Chain
 *
 * Implements automatic provider fallback when primary provider fails.
 * Features:
 * - Circuit breaker pattern for unhealthy provider detection
 * - Automatic provider promotion when primary fails
 * - Health tracking (failure rate, response time)
 * - Graceful fallback to alternatives
 * - Auto-recovery after cooldown period
 */
import { EventEmitter } from 'events';
// ============================================================================
// Default Configuration
// ============================================================================
const DEFAULT_CONFIG = {
    maxFailures: 3,
    cooldownMs: 60000, // 1 minute
    failureWindowMs: 300000, // 5 minutes
    slowThresholdMs: 5000, // 5 seconds
    autoPromote: true,
    maxSlowResponses: 5,
};
// ============================================================================
// ProviderFallbackChain Class
// ============================================================================
/**
 * Manages a fallback chain of AI providers with automatic failover,
 * health tracking, and circuit breaker functionality.
 */
export class ProviderFallbackChain extends EventEmitter {
    chain = [];
    config;
    metrics = new Map();
    currentIndex = 0;
    constructor(config = {}) {
        super();
        this.config = { ...DEFAULT_CONFIG, ...config };
    }
    // ==========================================================================
    // Chain Management
    // ==========================================================================
    /**
     * Sets the fallback chain order.
     * The first provider is the primary, subsequent providers are fallbacks.
     *
     * @param providers - Ordered list of providers (primary first)
     */
    setFallbackChain(providers) {
        if (providers.length === 0) {
            throw new Error('Fallback chain must have at least one provider');
        }
        this.chain = [...providers];
        this.currentIndex = 0;
        // Initialize metrics for any new providers
        for (const provider of providers) {
            if (!this.metrics.has(provider)) {
                this.initializeMetrics(provider);
            }
        }
    }
    /**
     * Gets the current fallback chain.
     */
    getFallbackChain() {
        return [...this.chain];
    }
    /**
     * Gets the current primary provider.
     */
    getPrimaryProvider() {
        return this.chain[0] ?? null;
    }
    // ==========================================================================
    // Provider Selection
    // ==========================================================================
    /**
     * Gets the next healthy provider in the chain.
     * Implements circuit breaker pattern - skips unhealthy providers.
     *
     * @param skipCurrent - Whether to skip the current provider
     * @returns The next available provider, or null if all are exhausted
     */
    getNextProvider(skipCurrent = false) {
        if (this.chain.length === 0) {
            return null;
        }
        const startIndex = skipCurrent ? this.currentIndex + 1 : this.currentIndex;
        const attemptedProviders = [];
        for (let i = 0; i < this.chain.length; i++) {
            const index = (startIndex + i) % this.chain.length;
            const provider = this.chain[index];
            attemptedProviders.push(provider);
            if (this.isProviderHealthy(provider)) {
                if (index !== this.currentIndex) {
                    const previousProvider = this.chain[this.currentIndex];
                    this.currentIndex = index;
                    this.emit('provider:fallback', {
                        from: previousProvider,
                        to: provider,
                        reason: skipCurrent ? 'explicit_skip' : 'health_check',
                    });
                }
                return provider;
            }
            // Check if we should attempt recovery
            if (this.shouldAttemptRecovery(provider)) {
                this.currentIndex = index;
                return provider;
            }
        }
        // All providers exhausted
        this.emit('chain:exhausted', { attemptedProviders });
        return null;
    }
    /**
     * Gets the current active provider (may be unhealthy).
     */
    getCurrentProvider() {
        return this.chain[this.currentIndex] ?? null;
    }
    // ==========================================================================
    // Health Tracking
    // ==========================================================================
    /**
     * Records a successful request for a provider.
     *
     * @param provider - The provider that succeeded
     * @param responseTimeMs - Response time in milliseconds
     */
    recordSuccess(provider, responseTimeMs) {
        const metrics = this.getOrCreateMetrics(provider);
        const now = Date.now();
        // Clean old entries
        this.cleanOldEntries(metrics);
        metrics.successes.push(now);
        metrics.totalRequests++;
        metrics.lastSuccess = now;
        metrics.responseTimes.push(responseTimeMs);
        // Keep only last 100 response times
        if (metrics.responseTimes.length > 100) {
            metrics.responseTimes.shift();
        }
        // Track slow responses
        if (responseTimeMs > this.config.slowThresholdMs) {
            metrics.consecutiveSlowResponses++;
        }
        else {
            metrics.consecutiveSlowResponses = 0;
        }
        // Check for recovery from unhealthy state
        if (metrics.circuitOpenedAt !== null) {
            metrics.circuitOpenedAt = null;
            this.emit('provider:recovered', { provider });
        }
        this.emit('provider:success', { provider, responseTimeMs });
    }
    /**
     * Records a failed request for a provider.
     *
     * @param provider - The provider that failed
     * @param error - Error message or description
     */
    recordFailure(provider, error) {
        const metrics = this.getOrCreateMetrics(provider);
        const now = Date.now();
        // Clean old entries
        this.cleanOldEntries(metrics);
        metrics.failures.push(now);
        metrics.totalRequests++;
        metrics.lastFailure = now;
        metrics.consecutiveSlowResponses = 0;
        this.emit('provider:failure', { provider, error });
        // Check if we should open the circuit
        if (metrics.failures.length >= this.config.maxFailures && metrics.circuitOpenedAt === null) {
            metrics.circuitOpenedAt = now;
            this.emit('provider:unhealthy', {
                provider,
                failureCount: metrics.failures.length,
                reason: `Exceeded ${this.config.maxFailures} failures within window`,
            });
            // Auto-promote if enabled and this is the primary
            if (this.config.autoPromote && this.chain[0] === provider) {
                this.promoteNextHealthyProvider();
            }
        }
    }
    /**
     * Gets the health status for a specific provider.
     *
     * @param provider - The provider to check
     * @returns Health status object
     */
    getHealthStatus(provider) {
        const metrics = this.getOrCreateMetrics(provider);
        this.cleanOldEntries(metrics);
        const totalInWindow = metrics.failures.length + metrics.successes.length;
        const failureRate = totalInWindow > 0 ? metrics.failures.length / totalInWindow : 0;
        const avgResponseTime = metrics.responseTimes.length > 0
            ? metrics.responseTimes.reduce((a, b) => a + b, 0) / metrics.responseTimes.length
            : 0;
        return {
            provider,
            healthy: this.isProviderHealthy(provider),
            failureCount: metrics.failures.length,
            successCount: metrics.successes.length,
            totalRequests: metrics.totalRequests,
            avgResponseTimeMs: Math.round(avgResponseTime),
            lastSuccess: metrics.lastSuccess,
            lastFailure: metrics.lastFailure,
            circuitOpenedAt: metrics.circuitOpenedAt,
            failureRate: Math.round(failureRate * 100) / 100,
            consecutiveSlowResponses: metrics.consecutiveSlowResponses,
        };
    }
    /**
     * Gets health status for all providers in the chain.
     */
    getAllHealthStatus() {
        return this.chain.map(provider => this.getHealthStatus(provider));
    }
    /**
     * Checks if a provider is currently healthy.
     * A provider is healthy if:
     * - Circuit is closed (not in cooldown)
     * - Not exceeding slow response threshold
     */
    isProviderHealthy(provider) {
        const metrics = this.metrics.get(provider);
        if (!metrics) {
            return true; // Unknown provider is assumed healthy
        }
        // Check if circuit is open
        if (metrics.circuitOpenedAt !== null) {
            return false;
        }
        // Check consecutive slow responses
        if (metrics.consecutiveSlowResponses >= this.config.maxSlowResponses) {
            return false;
        }
        return true;
    }
    // ==========================================================================
    // Circuit Breaker
    // ==========================================================================
    /**
     * Checks if we should attempt recovery for an unhealthy provider.
     * Recovery is attempted after the cooldown period.
     */
    shouldAttemptRecovery(provider) {
        const metrics = this.metrics.get(provider);
        if (!metrics || metrics.circuitOpenedAt === null) {
            return false;
        }
        const now = Date.now();
        const timeSinceOpen = now - metrics.circuitOpenedAt;
        return timeSinceOpen >= this.config.cooldownMs;
    }
    /**
     * Manually reset a provider's health status.
     * Use with caution - bypasses the circuit breaker.
     */
    resetProvider(provider) {
        this.initializeMetrics(provider);
        this.emit('provider:recovered', { provider });
    }
    /**
     * Manually mark a provider as unhealthy.
     * Opens the circuit breaker for this provider.
     */
    markUnhealthy(provider, reason) {
        const metrics = this.getOrCreateMetrics(provider);
        metrics.circuitOpenedAt = Date.now();
        this.emit('provider:unhealthy', {
            provider,
            failureCount: metrics.failures.length,
            reason,
        });
    }
    // ==========================================================================
    // Provider Promotion
    // ==========================================================================
    /**
     * Promotes a specific provider to primary position.
     */
    promoteProvider(provider) {
        const index = this.chain.indexOf(provider);
        if (index === -1) {
            throw new Error(`Provider ${provider} not in fallback chain`);
        }
        if (index === 0) {
            return; // Already primary
        }
        const previousPrimary = this.chain[0];
        this.chain.splice(index, 1);
        this.chain.unshift(provider);
        this.currentIndex = 0;
        this.emit('provider:promoted', { provider, previousPrimary });
    }
    /**
     * Promotes the next healthy provider to primary position.
     */
    promoteNextHealthyProvider() {
        for (let i = 1; i < this.chain.length; i++) {
            const provider = this.chain[i];
            if (this.isProviderHealthy(provider)) {
                this.promoteProvider(provider);
                return;
            }
        }
    }
    // ==========================================================================
    // Utility Methods
    // ==========================================================================
    /**
     * Initializes metrics for a provider.
     */
    initializeMetrics(provider) {
        this.metrics.set(provider, {
            failures: [],
            successes: [],
            responseTimes: [],
            totalRequests: 0,
            lastSuccess: null,
            lastFailure: null,
            circuitOpenedAt: null,
            consecutiveSlowResponses: 0,
        });
    }
    /**
     * Gets or creates metrics for a provider.
     */
    getOrCreateMetrics(provider) {
        if (!this.metrics.has(provider)) {
            this.initializeMetrics(provider);
        }
        return this.metrics.get(provider);
    }
    /**
     * Cleans old entries outside the failure window.
     */
    cleanOldEntries(metrics) {
        const now = Date.now();
        const cutoff = now - this.config.failureWindowMs;
        metrics.failures = metrics.failures.filter(t => t > cutoff);
        metrics.successes = metrics.successes.filter(t => t > cutoff);
    }
    /**
     * Updates the configuration.
     */
    updateConfig(config) {
        this.config = { ...this.config, ...config };
    }
    /**
     * Gets the current configuration.
     */
    getConfig() {
        return { ...this.config };
    }
    /**
     * Resets all metrics and state.
     */
    reset() {
        this.metrics.clear();
        this.currentIndex = 0;
        for (const provider of this.chain) {
            this.initializeMetrics(provider);
        }
    }
    /**
     * Disposes of the fallback chain and clears all listeners.
     */
    dispose() {
        this.chain = [];
        this.metrics.clear();
        this.currentIndex = 0;
        this.removeAllListeners();
    }
}
// ============================================================================
// Singleton Instance
// ============================================================================
let fallbackChainInstance = null;
/**
 * Gets the singleton instance of the ProviderFallbackChain.
 */
export function getFallbackChain() {
    if (!fallbackChainInstance) {
        fallbackChainInstance = new ProviderFallbackChain();
    }
    return fallbackChainInstance;
}
/**
 * Resets the singleton instance (useful for testing).
 */
export function resetFallbackChain() {
    if (fallbackChainInstance) {
        fallbackChainInstance.dispose();
    }
    fallbackChainInstance = null;
}
//# sourceMappingURL=fallback-chain.js.map