/**
 * Lightweight circuit breaker for external service calls.
 *
 * States:
 *   CLOSED  → normal operation; failures increment counter
 *   OPEN    → calls fail fast; after `resetTimeMs` → HALF_OPEN
 *   HALF_OPEN → one probe call allowed; success → CLOSED, failure → OPEN
 *
 * Does NOT depend on any NestJS or framework-specific API so it can
 * be used as a plain utility across the codebase.
 */

export enum CircuitState {
  CLOSED = 'CLOSED',
  OPEN = 'OPEN',
  HALF_OPEN = 'HALF_OPEN',
}

export interface CircuitBreakerOptions {
  /** Human-readable service name — used in logs / metrics. */
  name: string;
  /** Number of consecutive failures before opening the circuit. Default 5. */
  failureThreshold?: number;
  /** Time in ms to wait before transitioning from OPEN → HALF_OPEN. Default 30 000 (30s). */
  resetTimeMs?: number;
  /** Optional callback fired on every state transition. */
  onStateChange?: (from: CircuitState, to: CircuitState, name: string) => void;
}

export class CircuitBreaker {
  readonly name: string;

  private state = CircuitState.CLOSED;
  private failureCount = 0;
  private lastFailureTime = 0;

  private readonly failureThreshold: number;
  private readonly resetTimeMs: number;
  private readonly onStateChange?: (from: CircuitState, to: CircuitState, name: string) => void;

  constructor(options: CircuitBreakerOptions) {
    this.name = options.name;
    this.failureThreshold = options.failureThreshold ?? 5;
    this.resetTimeMs = options.resetTimeMs ?? 30_000;
    this.onStateChange = options.onStateChange;
  }

  /** Current state of the circuit. */
  getState(): CircuitState {
    this.evaluateState();
    return this.state;
  }

  /** Whether the circuit allows a call to pass through. */
  isAvailable(): boolean {
    this.evaluateState();
    return this.state !== CircuitState.OPEN;
  }

  /**
   * Execute `fn` through the breaker.
   *
   * - CLOSED / HALF_OPEN → `fn` is called.
   * - OPEN → immediately throws `CircuitOpenError`.
   *
   * On success the breaker resets to CLOSED.
   * On failure the breaker increments the failure counter (CLOSED)
   * or re-opens (HALF_OPEN).
   */
  async exec<T>(fn: () => Promise<T>): Promise<T> {
    this.evaluateState();

    if (this.state === CircuitState.OPEN) {
      throw new CircuitOpenError(this.name);
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (err) {
      this.onFailure();
      throw err;
    }
  }

  /** Record a manual success (e.g. from a health-check probe). */
  recordSuccess(): void {
    this.onSuccess();
  }

  /** Record a manual failure. */
  recordFailure(): void {
    this.onFailure();
  }

  /** Force-reset the breaker to CLOSED. */
  reset(): void {
    this.transitionTo(CircuitState.CLOSED);
    this.failureCount = 0;
    this.lastFailureTime = 0;
  }

  // -------------------------------------------------------------------------

  private evaluateState(): void {
    if (this.state === CircuitState.OPEN) {
      const elapsed = Date.now() - this.lastFailureTime;
      if (elapsed >= this.resetTimeMs) {
        this.transitionTo(CircuitState.HALF_OPEN);
      }
    }
  }

  private onSuccess(): void {
    if (this.state !== CircuitState.CLOSED) {
      this.transitionTo(CircuitState.CLOSED);
    }
    this.failureCount = 0;
  }

  private onFailure(): void {
    this.failureCount++;
    this.lastFailureTime = Date.now();

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

  private transitionTo(next: CircuitState): void {
    if (this.state === next) return;
    const prev = this.state;
    this.state = next;
    this.onStateChange?.(prev, next, this.name);
  }
}

/** Thrown when a call is attempted while the circuit is OPEN. */
export class CircuitOpenError extends Error {
  constructor(public readonly serviceName: string) {
    super(`Circuit breaker OPEN for service: ${serviceName}`);
    this.name = 'CircuitOpenError';
  }
}
