"use strict";
/**
 * Circuit Breaker Pattern Implementation
 * Prevents cascading failures and enables graceful degradation
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.CircuitBreakerManager = exports.CircuitBreakerTimeoutError = exports.CircuitBreakerOpenError = exports.CircuitBreaker = exports.CircuitState = void 0;
exports.getGlobalCircuitBreakerManager = getGlobalCircuitBreakerManager;
exports.shutdownGlobalCircuitBreakerManager = shutdownGlobalCircuitBreakerManager;
const events_1 = require("events");
const perf_hooks_1 = require("perf_hooks");
const logger_1 = require("../utils/logger");
var CircuitState;
(function (CircuitState) {
    CircuitState["CLOSED"] = "closed";
    CircuitState["OPEN"] = "open";
    CircuitState["HALF_OPEN"] = "half-open";
})(CircuitState || (exports.CircuitState = CircuitState = {}));
class CircuitBreaker extends events_1.EventEmitter {
    state = CircuitState.CLOSED;
    failureCount = 0;
    successCount = 0;
    totalCalls = 0;
    lastFailureTime = 0;
    lastSuccessTime = 0;
    stateChangeTime = Date.now();
    recentCalls = [];
    resetTimer;
    options;
    operation;
    name;
    constructor(operation, name, options = {}) {
        super();
        this.operation = operation;
        this.name = name;
        this.options = {
            failureThreshold: options.failureThreshold || 5,
            resetTimeout: options.resetTimeout || 60000, // 1 minute
            monitoringPeriod: options.monitoringPeriod || 60000, // 1 minute
            successThreshold: options.successThreshold || 3,
            timeoutMs: options.timeoutMs || 30000, // 30 seconds
            volumeThreshold: options.volumeThreshold || 10,
        };
    }
    /**
     * Execute the wrapped operation with circuit breaker protection
     */
    async execute(...args) {
        this.cleanupOldCalls();
        if (this.state === CircuitState.OPEN) {
            this.emit('circuit-open-rejection', { name: this.name, args });
            throw new CircuitBreakerOpenError(`Circuit breaker ${this.name} is OPEN`);
        }
        const startTime = perf_hooks_1.performance.now();
        this.totalCalls++;
        try {
            // Add timeout protection
            const result = await this.executeWithTimeout(args);
            const executionTime = perf_hooks_1.performance.now() - startTime;
            this.onSuccess(executionTime);
            return result;
        }
        catch (error) {
            const executionTime = perf_hooks_1.performance.now() - startTime;
            this.onFailure(error, executionTime);
            throw error;
        }
    }
    /**
     * Execute operation with timeout
     */
    async executeWithTimeout(args) {
        return new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                reject(new CircuitBreakerTimeoutError(`Operation ${this.name} timed out after ${this.options.timeoutMs}ms`));
            }, this.options.timeoutMs);
            this.operation(...args)
                .then((result) => {
                clearTimeout(timeout);
                resolve(result);
            })
                .catch((error) => {
                clearTimeout(timeout);
                reject(error);
            });
        });
    }
    /**
     * Handle successful execution
     */
    onSuccess(executionTime) {
        this.successCount++;
        this.lastSuccessTime = Date.now();
        this.recentCalls.push({ success: true, timestamp: Date.now() });
        this.emit('success', {
            name: this.name,
            executionTime,
            state: this.state,
            metrics: this.getMetrics(),
        });
        if (this.state === CircuitState.HALF_OPEN) {
            if (this.successCount >= this.options.successThreshold) {
                this.transitionTo(CircuitState.CLOSED);
            }
        }
    }
    /**
     * Handle failed execution
     */
    onFailure(error, executionTime) {
        this.failureCount++;
        this.lastFailureTime = Date.now();
        this.recentCalls.push({ success: false, timestamp: Date.now() });
        this.emit('failure', {
            name: this.name,
            error: error.message,
            executionTime,
            state: this.state,
            metrics: this.getMetrics(),
        });
        if (this.state === CircuitState.CLOSED || this.state === CircuitState.HALF_OPEN) {
            if (this.shouldOpenCircuit()) {
                this.transitionTo(CircuitState.OPEN);
            }
        }
    }
    /**
     * Determine if circuit should open based on failure criteria
     */
    shouldOpenCircuit() {
        const recentFailures = this.recentCalls.filter((call) => !call.success).length;
        const failureRate = this.recentCalls.length > 0 ? recentFailures / this.recentCalls.length : 0;
        // Need minimum volume and either threshold failures or high failure rate
        return (this.recentCalls.length >= this.options.volumeThreshold &&
            (recentFailures >= this.options.failureThreshold || failureRate > 0.5));
    }
    /**
     * Transition circuit to new state
     */
    transitionTo(newState) {
        const oldState = this.state;
        this.state = newState;
        this.stateChangeTime = Date.now();
        logger_1.logger.info(`Circuit breaker ${this.name} transitioned from ${oldState} to ${newState}`);
        this.emit('state-change', {
            name: this.name,
            oldState,
            newState,
            metrics: this.getMetrics(),
        });
        if (newState === CircuitState.OPEN) {
            this.scheduleReset();
        }
        else if (newState === CircuitState.CLOSED) {
            this.reset();
        }
    }
    /**
     * Schedule transition to half-open state
     */
    scheduleReset() {
        if (this.resetTimer) {
            clearTimeout(this.resetTimer);
        }
        this.resetTimer = setTimeout(() => {
            if (this.state === CircuitState.OPEN) {
                this.transitionTo(CircuitState.HALF_OPEN);
            }
        }, this.options.resetTimeout);
    }
    /**
     * Reset circuit breaker metrics
     */
    reset() {
        this.failureCount = 0;
        this.successCount = 0;
        this.recentCalls = [];
        if (this.resetTimer) {
            clearTimeout(this.resetTimer);
            this.resetTimer = undefined;
        }
    }
    /**
     * Clean up old call records outside monitoring period
     */
    cleanupOldCalls() {
        const cutoff = Date.now() - this.options.monitoringPeriod;
        this.recentCalls = this.recentCalls.filter((call) => call.timestamp > cutoff);
    }
    /**
     * Force circuit to specific state (for testing/recovery)
     */
    forceState(state) {
        this.transitionTo(state);
    }
    /**
     * Get current circuit breaker metrics
     */
    getMetrics() {
        this.cleanupOldCalls();
        const recentFailures = this.recentCalls.filter((call) => !call.success).length;
        const failureRate = this.recentCalls.length > 0 ? recentFailures / this.recentCalls.length : 0;
        return {
            state: this.state,
            failureCount: this.failureCount,
            successCount: this.successCount,
            totalCalls: this.totalCalls,
            lastFailureTime: this.lastFailureTime,
            lastSuccessTime: this.lastSuccessTime,
            stateChangeTime: this.stateChangeTime,
            failureRate,
        };
    }
    /**
     * Check if circuit is available for requests
     */
    isAvailable() {
        return this.state !== CircuitState.OPEN;
    }
    /**
     * Get current state
     */
    getState() {
        return this.state;
    }
    /**
     * Shutdown circuit breaker
     */
    shutdown() {
        if (this.resetTimer) {
            clearTimeout(this.resetTimer);
            this.resetTimer = undefined;
        }
        this.emit('shutdown', { name: this.name });
    }
}
exports.CircuitBreaker = CircuitBreaker;
/**
 * Circuit breaker specific error types
 */
class CircuitBreakerOpenError extends Error {
    constructor(message) {
        super(message);
        this.name = 'CircuitBreakerOpenError';
    }
}
exports.CircuitBreakerOpenError = CircuitBreakerOpenError;
class CircuitBreakerTimeoutError extends Error {
    constructor(message) {
        super(message);
        this.name = 'CircuitBreakerTimeoutError';
    }
}
exports.CircuitBreakerTimeoutError = CircuitBreakerTimeoutError;
/**
 * Circuit breaker manager for multiple operations
 */
class CircuitBreakerManager extends events_1.EventEmitter {
    breakers = new Map();
    /**
     * Create or get circuit breaker for operation
     */
    getCircuitBreaker(name, operation, options) {
        let breaker = this.breakers.get(name);
        if (!breaker) {
            breaker = new CircuitBreaker(operation, name, options);
            this.breakers.set(name, breaker);
            // Forward events
            breaker.on('state-change', (data) => this.emit('breaker-state-change', data));
            breaker.on('failure', (data) => this.emit('breaker-failure', data));
            breaker.on('success', (data) => this.emit('breaker-success', data));
        }
        return breaker;
    }
    /**
     * Get all circuit breaker metrics
     */
    getAllMetrics() {
        const metrics = {};
        for (const [name, breaker] of this.breakers) {
            metrics[name] = breaker.getMetrics();
        }
        return metrics;
    }
    /**
     * Get overall system health
     */
    getSystemHealth() {
        const metrics = this.getAllMetrics();
        const breakers = Object.values(metrics);
        const openCount = breakers.filter((b) => b.state === CircuitState.OPEN).length;
        const halfOpenCount = breakers.filter((b) => b.state === CircuitState.HALF_OPEN).length;
        const closedCount = breakers.filter((b) => b.state === CircuitState.CLOSED).length;
        const overallHealth = breakers.length > 0 ? (closedCount / breakers.length) * 100 : 100;
        return {
            totalBreakers: breakers.length,
            openBreakers: openCount,
            halfOpenBreakers: halfOpenCount,
            closedBreakers: closedCount,
            overallHealth,
        };
    }
    /**
     * Force all circuits to closed state (emergency recovery)
     */
    forceAllClosed() {
        for (const breaker of this.breakers.values()) {
            breaker.forceState(CircuitState.CLOSED);
        }
        logger_1.logger.warn('Forced all circuit breakers to CLOSED state');
        this.emit('emergency-reset');
    }
    /**
     * Shutdown all circuit breakers
     */
    shutdown() {
        for (const breaker of this.breakers.values()) {
            breaker.shutdown();
        }
        this.breakers.clear();
        this.emit('shutdown');
    }
}
exports.CircuitBreakerManager = CircuitBreakerManager;
/**
 * Global circuit breaker manager
 */
let globalCircuitBreakerManager = null;
function getGlobalCircuitBreakerManager() {
    if (!globalCircuitBreakerManager) {
        globalCircuitBreakerManager = new CircuitBreakerManager();
    }
    return globalCircuitBreakerManager;
}
function shutdownGlobalCircuitBreakerManager() {
    if (globalCircuitBreakerManager) {
        globalCircuitBreakerManager.shutdown();
        globalCircuitBreakerManager = null;
    }
}
//# sourceMappingURL=circuit-breaker.js.map