export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';

export interface CircuitBreakerMetrics {
  state: CircuitState;
  failureCount: number;
  successCount: number;
  lastFailureTime: Date | null;
  lastSuccessTime: Date | null;
  stateChangedAt: Date | null;
  totalExecutions: number;
  totalFailures: number;
  totalSuccesses: number;
}

export interface CircuitBreakerOptions {
  failureThreshold?: number;
  successThreshold?: number;
  timeout?: number;
  onStateChange?: (state: CircuitState, previousState: CircuitState) => void;
}

const DEFAULT_FAILURE_THRESHOLD = 5;
const DEFAULT_SUCCESS_THRESHOLD = 3;
const DEFAULT_TIMEOUT_MS = 60000;

export class CircuitBreakerError extends Error {
  public readonly state: CircuitState;

  constructor(message: string, state: CircuitState) {
    super(message);
    this.name = 'CircuitBreakerError';
    this.state = state;
  }
}

export class CircuitBreaker {
  private state: CircuitState = 'CLOSED';
  private failureCount = 0;
  private successCount = 0;
  private lastFailureTime: Date | null = null;
  private lastSuccessTime: Date | null = null;
  private stateChangedAt: Date | null = null;
  private totalExecutions = 0;
  private totalFailures = 0;
  private totalSuccesses = 0;

  private readonly failureThreshold: number;
  private readonly successThreshold: number;
  private readonly timeout: number;
  private readonly onStateChange?: (state: CircuitState, previousState: CircuitState) => void;

  constructor(options?: CircuitBreakerOptions) {
    this.failureThreshold = options?.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
    this.successThreshold = options?.successThreshold ?? DEFAULT_SUCCESS_THRESHOLD;
    this.timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
    this.onStateChange = options?.onStateChange;
    this.stateChangedAt = new Date();
  }

  public getState(): CircuitState {
    if (this.state === 'OPEN') {
      const elapsed = Date.now() - this.lastFailureTime!.getTime();
      if (elapsed >= this.timeout) {
        this.transitionTo('HALF_OPEN');
      }
    }
    return this.state;
  }

  public async execute<T>(
    fn: () => Promise<T>,
    fallback?: () => T | Promise<T>
  ): Promise<T> {
    this.totalExecutions++;
    const currentState = this.getState();

    try {
      let result: T;

      if (currentState === 'OPEN') {
        throw new CircuitBreakerError(
          `Circuit is OPEN. Failures: ${this.failureCount}/${this.failureThreshold}`,
          this.state
        );
      }

      result = await fn();
      this.recordSuccess();
      return result;
    } catch (error) {
      this.recordFailure();

      if (fallback) {
        try {
          return fallback();
        } catch (fallbackError) {
          throw error;
        }
      }

      throw error;
    }
  }

  public getMetrics(): CircuitBreakerMetrics {
    return {
      state: this.getState(),
      failureCount: this.failureCount,
      successCount: this.successCount,
      lastFailureTime: this.lastFailureTime,
      lastSuccessTime: this.lastSuccessTime,
      stateChangedAt: this.stateChangedAt,
      totalExecutions: this.totalExecutions,
      totalFailures: this.totalFailures,
      totalSuccesses: this.totalSuccesses,
    };
  }

  public reset(): void {
    const previousState = this.state;
    this.state = 'CLOSED';
    this.failureCount = 0;
    this.successCount = 0;
    this.lastFailureTime = null;
    this.lastSuccessTime = null;
    this.stateChangedAt = new Date();
    if (previousState !== 'CLOSED') {
      this.emitStateChange('CLOSED', previousState);
    }
  }

  private recordSuccess(): void {
    this.lastSuccessTime = new Date();
    this.totalSuccesses++;

    if (this.state === 'HALF_OPEN') {
      this.successCount++;
      if (this.successCount >= this.successThreshold) {
        this.transitionTo('CLOSED');
        this.failureCount = 0;
        this.successCount = 0;
      }
    }
  }

  private recordFailure(): void {
    this.lastFailureTime = new Date();
    this.totalFailures++;
    this.failureCount++;

    if (this.state === 'HALF_OPEN') {
      this.transitionTo('OPEN');
    } else if (this.state === 'CLOSED' && this.failureCount >= this.failureThreshold) {
      this.transitionTo('OPEN');
    }
  }

  private transitionTo(newState: CircuitState): void {
    const previousState = this.state;
    this.state = newState;
    this.stateChangedAt = new Date();
    if (newState === 'CLOSED') {
      this.successCount = 0;
    }
    this.emitStateChange(newState, previousState);
  }

  private emitStateChange(newState: CircuitState, previousState: CircuitState): void {
    if (this.onStateChange && newState !== previousState) {
      this.onStateChange(newState, previousState);
    }
  }
}
