'use strict';

/**
 * circuit-breaker.js — Circuit breaker for API resilience.
 *
 * States:
 *   CLOSED   — Normal operation. Failures tracked.
 *   OPEN     — Too many failures. Calls rejected immediately.
 *   HALF_OPEN — Cooldown expired. One probe call allowed.
 *
 * Transition rules:
 *   CLOSED  -> OPEN:      failures >= threshold
 *   OPEN    -> HALF_OPEN: cooldown expired
 *   HALF_OPEN -> CLOSED:  probe succeeds
 *   HALF_OPEN -> OPEN:    probe fails
 */

const STATE = {
  CLOSED: 'closed',
  OPEN: 'open',
  HALF_OPEN: 'half_open',
};

class CircuitBreaker {
  /**
   * @param {object} opts
   * @param {number} opts.threshold - Consecutive failures to open (default 5)
   * @param {number} opts.cooldownMs - Milliseconds before half-open probe (default 30000)
   * @param {Function} [opts.onStateChange] - Called with (newState, oldState)
   * @param {object} [opts.logger] - Optional logger
   */
  constructor(opts = {}) {
    this.threshold = opts.threshold || 5;
    this.cooldownMs = opts.cooldownMs || 30000;
    this.onStateChange = opts.onStateChange || null;
    this.logger = opts.logger || null;

    this.state = STATE.CLOSED;
    this.failures = 0;
    this.openedAt = null;
    this.lastError = null;
  }

  /**
   * Execute a function through the circuit breaker.
   * Throws CircuitOpenError if the circuit is open.
   * @param {Function} fn - Async function to execute
   * @returns {Promise<*>}
   */
  async execute(fn) {
    if (this.state === STATE.OPEN) {
      const elapsed = Date.now() - this.openedAt;
      if (elapsed >= this.cooldownMs) {
        this._transition(STATE.HALF_OPEN);
      } else {
        throw new CircuitOpenError(
          `Circuit open (${Math.ceil((this.cooldownMs - elapsed) / 1000)}s remaining)`,
          this.lastError,
        );
      }
    }

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

  _onSuccess() {
    if (this.state === STATE.HALF_OPEN) {
      this._transition(STATE.CLOSED);
    }
    this.failures = 0;
    this.lastError = null;
  }

  _onFailure(err) {
    this.lastError = err;
    this.failures++;

    if (this.state === STATE.HALF_OPEN) {
      // Probe failed — reopen
      this.openedAt = Date.now();
      this._transition(STATE.OPEN);
    } else if (this.state === STATE.CLOSED && this.failures >= this.threshold) {
      this.openedAt = Date.now();
      this._transition(STATE.OPEN);
    }
  }

  _transition(newState) {
    const oldState = this.state;
    this.state = newState;

    if (newState === STATE.CLOSED) {
      this.failures = 0;
      this.openedAt = null;
    }

    if (this.logger) {
      this.logger.warn({ from: oldState, to: newState }, 'Circuit breaker state change');
    }

    if (this.onStateChange) {
      this.onStateChange(newState, oldState);
    }
  }

  getState() {
    return this.state;
  }

  getMetrics() {
    return {
      state: this.state,
      failures: this.failures,
      threshold: this.threshold,
      openedAt: this.openedAt,
      lastError: this.lastError ? this.lastError.message : null,
    };
  }

  reset() {
    this.state = STATE.CLOSED;
    this.failures = 0;
    this.openedAt = null;
    this.lastError = null;
  }
}

class CircuitOpenError extends Error {
  constructor(message, cause) {
    super(message);
    this.name = 'CircuitOpenError';
    this.cause = cause;
  }
}

module.exports = { CircuitBreaker, CircuitOpenError, STATE };
