/**
 * Circuit Breaker Pattern Implementation
 * Prevents cascading failures in autonomous loops and external service calls
 */

export enum CircuitState {
	CLOSED = 'CLOSED',     // Normal operation
	OPEN = 'OPEN',         // Circuit is open, calls fail fast
	HALF_OPEN = 'HALF_OPEN' // Testing if service has recovered
}

export interface CircuitBreakerConfig {
	failureThreshold?: number;     // Number of failures before opening circuit
	recoveryTimeout?: number;      // Time in ms before attempting recovery
	successThreshold?: number;     // Number of successes needed to close circuit in half-open state
	monitoringWindow?: number;     // Time window for failure counting in ms
	maxRetryAttempts?: number;     // Maximum retry attempts before giving up
	exponentialBackoff?: boolean; // Use exponential backoff for retries
	onStateChange?: (state: CircuitState, reason: string) => void;
	onFailure?: (error: Error) => void;
	onSuccess?: () => void;
}

export interface CircuitBreakerStats {
	state: CircuitState;
	failures: number;
	successes: number;
	totalCalls: number;
	failureRate: number;
	lastFailureTime?: number;
	lastSuccessTime?: number;
	uptime: number;
	stateChanges: number;
}

export interface FailureRecord {
	timestamp: number;
	error: string;
	attempts: number;
}

export class CircuitBreaker {
	private state: CircuitState = CircuitState.CLOSED;
	private failures: number = 0;
	private successes: number = 0;
	private totalCalls: number = 0;
	private lastFailureTime?: number;
	private lastSuccessTime?: number;
	private nextAttemptTime: number = 0;
	private stateChanges: number = 0;
	private createdAt: number = Date.now();
	private config: Required<CircuitBreakerConfig>;
	private recentFailures: FailureRecord[] = [];
	private recoveryTimer?: NodeJS.Timeout;

	constructor(private name: string, config: CircuitBreakerConfig = {}) {
		this.config = {
			failureThreshold: 5,
			recoveryTimeout: 60000, // 1 minute
			successThreshold: 3,
			monitoringWindow: 300000, // 5 minutes
			maxRetryAttempts: 3,
			exponentialBackoff: true,
			onStateChange: () => {},
			onFailure: () => {},
			onSuccess: () => {},
			...config
		};
	}

	/**
	 * Execute a function with circuit breaker protection
	 */
	public async execute<T>(
		operation: () => Promise<T>,
		fallback?: () => Promise<T> | T
	): Promise<T> {
		this.totalCalls++;

		// If circuit is open, fail fast
		if (this.state === CircuitState.OPEN) {
			if (Date.now() < this.nextAttemptTime) {
				const error = new Error(`Circuit breaker ${this.name} is OPEN. Next attempt at ${new Date(this.nextAttemptTime)}`);
				this.config.onFailure(error);
				
				if (fallback) {
					return await Promise.resolve(fallback());
				}
				throw error;
			} else {
				// Transition to half-open to test recovery
				this.setState(CircuitState.HALF_OPEN, 'Recovery timeout reached');
			}
		}

		try {
			const result = await this.executeWithRetry(operation);
			this.onSuccess();
			return result;
		} catch (error) {
			this.onFailure(error);
			
			if (fallback) {
				try {
					return await Promise.resolve(fallback());
				} catch (fallbackError) {
					throw error; // Throw original error if fallback fails
				}
			}
			
			throw error;
		}
	}

	/**
	 * Execute operation with retry logic
	 */
	private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
		let lastError: Error;
		let baseDelay = 1000; // Start with 1 second

		for (let attempt = 1; attempt <= this.config.maxRetryAttempts; attempt++) {
			try {
				return await operation();
			} catch (error) {
				lastError = error;
				
				// Don't retry on the last attempt
				if (attempt === this.config.maxRetryAttempts) {
					break;
				}

				// Calculate delay for next attempt
				const delay = this.config.exponentialBackoff 
					? baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000
					: baseDelay;

				await new Promise(resolve => setTimeout(resolve, Math.min(delay, 10000)));
			}
		}

		throw lastError!;
	}

	/**
	 * Handle successful operation
	 */
	private onSuccess(): void {
		this.successes++;
		this.lastSuccessTime = Date.now();
		this.config.onSuccess();

		if (this.state === CircuitState.HALF_OPEN) {
			if (this.successes >= this.config.successThreshold) {
				this.setState(CircuitState.CLOSED, 'Success threshold reached in half-open state');
				this.resetCounters();
			}
		} else if (this.state === CircuitState.CLOSED) {
			// Reset failure count on success in closed state
			this.failures = 0;
			this.recentFailures = [];
		}
	}

	/**
	 * Handle failed operation
	 */
	private onFailure(error: Error): void {
		this.failures++;
		this.lastFailureTime = Date.now();
		this.config.onFailure(error);

		// Record failure for analysis
		this.recentFailures.push({
			timestamp: Date.now(),
			error: error.message,
			attempts: this.config.maxRetryAttempts
		});

		// Clean up old failures outside monitoring window
		const cutoff = Date.now() - this.config.monitoringWindow;
		this.recentFailures = this.recentFailures.filter(f => f.timestamp > cutoff);

		// Check if we should open the circuit
		if (this.state === CircuitState.CLOSED || this.state === CircuitState.HALF_OPEN) {
			if (this.recentFailures.length >= this.config.failureThreshold) {
				this.setState(CircuitState.OPEN, `Failure threshold reached: ${this.recentFailures.length} failures`);
				this.scheduleRecovery();
			}
		}
	}

	/**
	 * Change circuit state with logging
	 */
	private setState(newState: CircuitState, reason: string): void {
		const oldState = this.state;
		this.state = newState;
		this.stateChanges++;
		
		console.log(`Circuit breaker ${this.name}: ${oldState} -> ${newState} (${reason})`);
		this.config.onStateChange(newState, reason);
	}

	/**
	 * Schedule recovery attempt
	 */
	private scheduleRecovery(): void {
		if (this.recoveryTimer) {
			clearTimeout(this.recoveryTimer);
		}

		this.nextAttemptTime = Date.now() + this.config.recoveryTimeout;
		
		this.recoveryTimer = setTimeout(() => {
			if (this.state === CircuitState.OPEN) {
				this.setState(CircuitState.HALF_OPEN, 'Recovery timer expired');
			}
		}, this.config.recoveryTimeout);
	}

	/**
	 * Reset failure/success counters
	 */
	private resetCounters(): void {
		this.failures = 0;
		this.successes = 0;
		this.recentFailures = [];
	}

	/**
	 * Manually open the circuit (for emergency situations)
	 */
	public open(reason: string = 'Manual intervention'): void {
		this.setState(CircuitState.OPEN, reason);
		this.scheduleRecovery();
	}

	/**
	 * Manually close the circuit (for recovery scenarios)
	 */
	public close(reason: string = 'Manual intervention'): void {
		this.setState(CircuitState.CLOSED, reason);
		this.resetCounters();
		
		if (this.recoveryTimer) {
			clearTimeout(this.recoveryTimer);
			this.recoveryTimer = undefined;
		}
	}

	/**
	 * Get current circuit breaker statistics
	 */
	public getStats(): CircuitBreakerStats {
		const now = Date.now();
		const windowStart = now - this.config.monitoringWindow;
		const recentFailureCount = this.recentFailures.filter(f => f.timestamp > windowStart).length;
		const recentCallCount = Math.max(recentFailureCount + this.successes, 1);
		
		return {
			state: this.state,
			failures: this.failures,
			successes: this.successes,
			totalCalls: this.totalCalls,
			failureRate: recentFailureCount / recentCallCount,
			lastFailureTime: this.lastFailureTime,
			lastSuccessTime: this.lastSuccessTime,
			uptime: now - this.createdAt,
			stateChanges: this.stateChanges
		};
	}

	/**
	 * Get recent failure history
	 */
	public getFailureHistory(): FailureRecord[] {
		return [...this.recentFailures];
	}

	/**
	 * Check if circuit is healthy
	 */
	public isHealthy(): boolean {
		const stats = this.getStats();
		return stats.state === CircuitState.CLOSED && stats.failureRate < 0.1; // Less than 10% failure rate
	}

	/**
	 * Clean up resources
	 */
	public destroy(): void {
		if (this.recoveryTimer) {
			clearTimeout(this.recoveryTimer);
			this.recoveryTimer = undefined;
		}
	}
}

/**
 * Circuit Breaker Registry for managing multiple circuit breakers
 */
export class CircuitBreakerRegistry {
	private static instance: CircuitBreakerRegistry;
	private breakers = new Map<string, CircuitBreaker>();

	private constructor() {}

	public static getInstance(): CircuitBreakerRegistry {
		if (!CircuitBreakerRegistry.instance) {
			CircuitBreakerRegistry.instance = new CircuitBreakerRegistry();
		}
		return CircuitBreakerRegistry.instance;
	}

	/**
	 * Get or create a circuit breaker
	 */
	public getBreaker(name: string, config?: CircuitBreakerConfig): CircuitBreaker {
		if (!this.breakers.has(name)) {
			this.breakers.set(name, new CircuitBreaker(name, config));
		}
		return this.breakers.get(name)!;
	}

	/**
	 * Remove a circuit breaker
	 */
	public removeBreaker(name: string): boolean {
		const breaker = this.breakers.get(name);
		if (breaker) {
			breaker.destroy();
			return this.breakers.delete(name);
		}
		return false;
	}

	/**
	 * Get all circuit breaker statistics
	 */
	public getAllStats(): Record<string, CircuitBreakerStats> {
		const stats: Record<string, CircuitBreakerStats> = {};
		for (const [name, breaker] of this.breakers.entries()) {
			stats[name] = breaker.getStats();
		}
		return stats;
	}

	/**
	 * Get health status of all circuit breakers
	 */
	public getHealthStatus(): { healthy: string[]; unhealthy: string[] } {
		const healthy: string[] = [];
		const unhealthy: string[] = [];

		for (const [name, breaker] of this.breakers.entries()) {
			if (breaker.isHealthy()) {
				healthy.push(name);
			} else {
				unhealthy.push(name);
			}
		}

		return { healthy, unhealthy };
	}

	/**
	 * Close all circuit breakers (emergency stop)
	 */
	public emergencyStop(): void {
		for (const [name, breaker] of this.breakers.entries()) {
			breaker.open(`Emergency stop triggered`);
		}
	}

	/**
	 * Reset all circuit breakers
	 */
	public resetAll(): void {
		for (const [name, breaker] of this.breakers.entries()) {
			breaker.close(`Global reset triggered`);
		}
	}

	/**
	 * Clean up all circuit breakers
	 */
	public destroy(): void {
		for (const breaker of this.breakers.values()) {
			breaker.destroy();
		}
		this.breakers.clear();
	}
}

// Global registry instance
export const circuitRegistry = CircuitBreakerRegistry.getInstance();