/**
 * Retry logic and circuit breaker for API requests
 */

import { ApiError } from './ApiError';

export interface RetryConfig {
  maxRetries: number;
  baseDelay: number; // milliseconds
  maxDelay: number; // milliseconds
  backoffMultiplier: number;
  timeout: number; // milliseconds
}

export const DEFAULT_RETRY_CONFIG: RetryConfig = {
  maxRetries: 1,
  baseDelay: 1000,
  maxDelay: 10000,
  backoffMultiplier: 2,
  timeout: 30000,
};

export interface CircuitBreakerConfig {
  failureThreshold: number;
  successThreshold: number;
  timeout: number; // milliseconds before trying again
}

export const DEFAULT_CIRCUIT_BREAKER_CONFIG: CircuitBreakerConfig = {
  failureThreshold: 5,
  successThreshold: 3,
  timeout: 60000, // 1 minute
};

export class CircuitBreaker {
  private failures = 0;
  private successes = 0;
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
  private lastFailureTime: number | null = null;
  private readonly config: CircuitBreakerConfig;

  constructor(config: CircuitBreakerConfig = DEFAULT_CIRCUIT_BREAKER_CONFIG) {
    this.config = config;
  }

  /**
   * Check if request should proceed
   */
  canExecute(): boolean {
    if (this.state === 'CLOSED') {
      return true;
    }

    if (this.state === 'OPEN') {
      const now = Date.now();
      if (this.lastFailureTime && now - this.lastFailureTime > this.config.timeout) {
        this.state = 'HALF_OPEN';
        return true;
      }
      return false;
    }

    // HALF_OPEN - allow one request to test if service recovered
    return true;
  }

  /**
   * Record a successful request
   */
  recordSuccess(): void {
    if (this.state === 'HALF_OPEN') {
      this.successes++;
      if (this.successes >= this.config.successThreshold) {
        this.reset();
      }
    } else if (this.state === 'CLOSED') {
      // Keep failures in check
      this.failures = Math.max(0, this.failures - 1);
    }
  }

  /**
   * Record a failed request
   */
  recordFailure(): void {
    this.failures++;
    this.lastFailureTime = Date.now();

    if (this.state === 'HALF_OPEN') {
      this.state = 'OPEN';
      this.successes = 0;
    } else if (this.state === 'CLOSED' && this.failures >= this.config.failureThreshold) {
      this.state = 'OPEN';
    }
  }

  /**
   * Get current state
   */
  getState(): string {
    return this.state;
  }

  /**
   * Get failure count
   */
  getFailures(): number {
    return this.failures;
  }

  /**
   * Reset the circuit breaker
   */
  reset(): void {
    this.failures = 0;
    this.successes = 0;
    this.state = 'CLOSED';
    this.lastFailureTime = null;
  }
}

export class RetryHandler {
  private config: RetryConfig;
  private abortController: AbortController | null = null;

  constructor(config: RetryConfig = DEFAULT_RETRY_CONFIG) {
    this.config = config;
  }

  /**
   * Execute a function with retry logic
   */
  async execute<T>(
    fn: (signal: AbortSignal) => Promise<T>,
    isRetryable?: (error: ApiError) => boolean
  ): Promise<T> {
    this.abortController = new AbortController();
    const signal = this.abortController.signal;

    let lastError: Error | null = null;
    
    for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
      try {
        // Check if already aborted
        if (signal.aborted) {
          throw new Error('Request aborted');
        }

        const result = await fn(signal);
        return result;
      } catch (error) {
        lastError = error as Error;

        // Check if we should abort
        if (signal.aborted) {
          throw error;
        }

        // Check if this is the last attempt
        if (attempt === this.config.maxRetries) {
          break;
        }

        // Check if error is retryable
        if (error instanceof ApiError) {
          if (!error.isRetryable()) {
            throw error;
          }
          if (isRetryable && !isRetryable(error)) {
            throw error;
          }
        }

        // Calculate delay with exponential backoff
        const delay = this.calculateDelay(attempt);
        
        // Wait before retrying
        await this.sleep(delay, signal);
      }
    }

    // All retries exhausted
    throw lastError;
  }

  /**
   * Cancel the current request
   */
  abort(): void {
    if (this.abortController) {
      this.abortController.abort();
    }
  }

  /**
   * Calculate delay for retry attempt
   */
  private calculateDelay(attempt: number): number {
    const delay = Math.min(
      this.config.baseDelay * Math.pow(this.config.backoffMultiplier, attempt),
      this.config.maxDelay
    );
    // Add jitter to prevent thundering herd
    const jitter = Math.random() * 0.3 * delay;
    return delay + jitter;
  }

  /**
   * Sleep with abort support
   */
  private sleep(ms: number, signal: AbortSignal): Promise<void> {
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        signal.removeEventListener('abort', abortHandler);
        resolve();
      }, ms);

      const abortHandler = () => {
        clearTimeout(timeout);
        signal.removeEventListener('abort', abortHandler);
        reject(new Error('Request aborted during retry delay'));
      };

      signal.addEventListener('abort', abortHandler, { once: true });
    });
  }
}

/**
 * Global circuit breaker registry for different API endpoints
 */
export class CircuitBreakerRegistry {
  private static instance: CircuitBreakerRegistry;
  private breakers: Map<string, CircuitBreaker> = new Map();

  private constructor() {}

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

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

  /**
   * Reset all circuit breakers
   */
  resetAll(): void {
    this.breakers.forEach(breaker => breaker.reset());
  }

  /**
   * Get status of all circuit breakers
   */
  getStatus(): Record<string, { state: string; failures: number }> {
    const status: Record<string, { state: string; failures: number }> = {};
    this.breakers.forEach((breaker, endpoint) => {
      status[endpoint] = {
        state: breaker.getState(),
        failures: breaker.getFailures(),
      };
    });
    return status;
  }
}