#!/usr/bin/env node
/**
 * Precompute Circuit Breaker — PR-10 Production Hardening
 *
 * In-memory circuit breaker that protects against precompute degradation.
 * When the breaker opens, replacement is forcibly disabled and all requests
 * use primary recall only.
 *
 * State machine:
 *
 *   ┌────────┐   consecutiveFailures >= 3   ┌──────┐
 *   │ CLOSED │  OR timeoutRate >= 5%        │ OPEN │
 *   │ (ok)   │  OR mismatchRate >= 10%      │(stop)│
 *   └───┬────┘ ────────────────────────────▶│      │
 *       │                                    └──┬───┘
 *       │          success                       │
 *       ◀─────────────────────────────────────   │ cooldown
 *       │                                        │ elapsed
 *       │                   ┌────────────┐       │
 *       │    success        │  HALF_OPEN │◀──────┘
 *       ◀──────────────────—│  (probe 1) │
 *                           └─────┬──────┘
 *                                 │ failure
 *                                 ▶──────── (back to OPEN)
 *
 * Mutable in-memory state. No file I/O, no database, no FTS5.
 *
 * @module precompute-circuit-breaker
 */

// ─── State Constants ──────────────────────────────────

export const CIRCUIT_CLOSED = "closed";
export const CIRCUIT_OPEN = "open";
export const CIRCUIT_HALF_OPEN = "half_open";

// ─── Default Thresholds ───────────────────────────────

const DEFAULT_OPTIONS = {
  /** Consecutive failures to trigger open */
  consecutiveFailureThreshold: 3,
  /** Timeout rate threshold (percentage 0-1) */
  timeoutRateThreshold: 0.05,
  /** Mismatch rate threshold (percentage 0-1) */
  mismatchRateThreshold: 0.10,
  /** Minimum samples before rate-based triggers activate */
  minSamplesForRateCheck: 20,
  /** Cooldown in ms before transitioning open → half_open */
  cooldownMs: 30000,
};

// ─── Circuit Breaker Factory ──────────────────────────

/**
 * Create an in-memory circuit breaker.
 *
 * @param {object} [options]
 * @param {number} [options.consecutiveFailureThreshold=3]
 * @param {number} [options.timeoutRateThreshold=0.05]
 * @param {number} [options.mismatchRateThreshold=0.10]
 * @param {number} [options.minSamplesForRateCheck=20]
 * @param {number} [options.cooldownMs=30000]
 * @returns {object} Circuit breaker instance
 */
export function createCircuitBreaker(options = {}) {
  const opts = { ...DEFAULT_OPTIONS, ...options };

  let state = CIRCUIT_CLOSED;
  let totalCalls = 0;
  let consecutiveFailures = 0;
  let totalFailures = 0;
  let totalTimeouts = 0;
  let totalMismatches = 0;
  let lastFailureReason = null;
  let openedAt = null;
  let lastStateChangeAt = Date.now();

  /**
   * Check if replacement should be allowed.
   * Also handles state transitions: open → half_open on cooldown.
   *
   * @returns {boolean} true if replacement is allowed
   */
  function shouldAllowReplacement() {
    if (state === CIRCUIT_CLOSED) {
      return true;
    }

    if (state === CIRCUIT_OPEN) {
      // Check cooldown: if enough time has passed, transition to half_open
      if (openedAt && (Date.now() - openedAt) >= opts.cooldownMs) {
        _transitionTo(CIRCUIT_HALF_OPEN, "cooldown elapsed");
        return true; // Allow one probe call
      }
      return false;
    }

    if (state === CIRCUIT_HALF_OPEN) {
      return true; // Allow probe
    }

    return false; // Unknown state → safe default: disallow
  }

  /**
   * Record a successful replacement result.
   * Resets consecutive failure count, transitions half_open → closed.
   */
  function recordSuccess() {
    totalCalls++;
    consecutiveFailures = 0;

    if (state === CIRCUIT_HALF_OPEN) {
      _transitionTo(CIRCUIT_CLOSED, "probe succeeded");
    }
  }

  /**
   * Record a replacement failure.
   * Increments failure counters, potentially opens the circuit.
   *
   * @param {string} [reason] - Description of the failure
   */
  function recordFailure(reason) {
    totalCalls++;
    consecutiveFailures++;
    totalFailures++;
    lastFailureReason = reason || null;

    if (state === CIRCUIT_HALF_OPEN) {
      // Probe failed → back to open
      _transitionTo(CIRCUIT_OPEN, `probe failed: ${reason || "unknown"}`);
      return;
    }

    if (state === CIRCUIT_CLOSED) {
      _checkTriggers();
    }
  }

  /**
   * Record a timeout event.
   */
  function recordTimeout() {
    totalCalls++;
    consecutiveFailures++;
    totalFailures++;
    totalTimeouts++;

    if (state === CIRCUIT_HALF_OPEN) {
      _transitionTo(CIRCUIT_OPEN, "probe timed out");
      return;
    }

    if (state === CIRCUIT_CLOSED) {
      _checkTriggers();
    }
  }

  /**
   * Record a result mismatch between primary and precompute.
   */
  function recordMismatch() {
    totalCalls++;
    consecutiveFailures++;
    totalFailures++;
    totalMismatches++;

    if (state === CIRCUIT_HALF_OPEN) {
      // Mismatch during probe → back to open
      _transitionTo(CIRCUIT_OPEN, "probe result mismatch");
      return;
    }

    if (state === CIRCUIT_CLOSED) {
      _checkTriggers();
    }
  }

  /**
   * Get current circuit state.
   * @returns {"closed"|"open"|"half_open"}
   */
  function getState() {
    // Might auto-transition to half_open if cooldown elapsed
    if (state === CIRCUIT_OPEN && openedAt && (Date.now() - openedAt) >= opts.cooldownMs) {
      _transitionTo(CIRCUIT_HALF_OPEN, "cooldown elapsed (checked via getState)");
    }
    return state;
  }

  /**
   * Get a snapshot of breaker metrics. Read-only.
   */
  function getSnapshot() {
    return {
      state: getState(),
      totalCalls,
      consecutiveFailures,
      totalFailures,
      totalTimeouts,
      totalMismatches,
      timeoutRate: totalCalls > 0 ? Math.round((totalTimeouts / totalCalls) * 10000) / 10000 : 0,
      mismatchRate: totalCalls > 0 ? Math.round((totalMismatches / totalCalls) * 10000) / 10000 : 0,
      lastFailureReason,
      openedAt: openedAt ? new Date(openedAt).toISOString() : null,
      lastStateChangeAt: new Date(lastStateChangeAt).toISOString(),
      cooldownMs: opts.cooldownMs,
      thresholds: {
        consecutiveFailureThreshold: opts.consecutiveFailureThreshold,
        timeoutRateThreshold: opts.timeoutRateThreshold,
        mismatchRateThreshold: opts.mismatchRateThreshold,
        minSamplesForRateCheck: opts.minSamplesForRateCheck,
      },
    };
  }

  /**
   * Reset circuit breaker to initial state.
   */
  function reset() {
    state = CIRCUIT_CLOSED;
    totalCalls = 0;
    consecutiveFailures = 0;
    totalFailures = 0;
    totalTimeouts = 0;
    totalMismatches = 0;
    lastFailureReason = null;
    openedAt = null;
    lastStateChangeAt = Date.now();
  }

  // ─── Internal ───────────────────────────────────────

  function _transitionTo(newState, reason) {
    state = newState;
    lastStateChangeAt = Date.now();
    if (newState === CIRCUIT_OPEN) {
      openedAt = Date.now();
    }
    // Only log transitions in verbose mode (not here — pure memory)
  }

  /**
   * Check if any trigger condition is met to open the circuit.
   */
  function _checkTriggers() {
    const reasons = [];

    // Trigger 1: consecutive failures >= threshold
    if (consecutiveFailures >= opts.consecutiveFailureThreshold) {
      reasons.push(`consecutive failures ${consecutiveFailures} >= ${opts.consecutiveFailureThreshold}`);
    }

    // Trigger 2: timeout rate too high (need enough samples)
    if (totalCalls >= opts.minSamplesForRateCheck) {
      const timeoutRate = totalTimeouts / totalCalls;
      if (timeoutRate >= opts.timeoutRateThreshold) {
        reasons.push(`timeout rate ${(timeoutRate * 100).toFixed(1)}% >= ${(opts.timeoutRateThreshold * 100).toFixed(0)}%`);
      }
    }

    // Trigger 3: mismatch rate too high (need enough samples)
    if (totalCalls >= opts.minSamplesForRateCheck) {
      const mismatchRate = totalMismatches / totalCalls;
      if (mismatchRate >= opts.mismatchRateThreshold) {
        reasons.push(`mismatch rate ${(mismatchRate * 100).toFixed(1)}% >= ${(opts.mismatchRateThreshold * 100).toFixed(0)}%`);
      }
    }

    if (reasons.length > 0) {
      _transitionTo(CIRCUIT_OPEN, reasons.join("; "));
    }
  }

  return {
    recordSuccess,
    recordFailure,
    recordTimeout,
    recordMismatch,
    shouldAllowReplacement,
    getState,
    getSnapshot,
    reset,
    // Exposed for testing
    _state: () => state,
    _counters: () => ({
      totalCalls, consecutiveFailures, totalFailures,
      totalTimeouts, totalMismatches,
    }),
  };
}
