/**
 * Per-engine circuit breaker for fleet routing (§2 cost/quality routing).
 *
 * Tracks failures per `(factoryId, engine)` pair. When a concrete engine
 * repeatedly fails on a given factory (e.g. `codex` erroring on a box that lacks
 * a working install), the breaker OPENS for that pair and the scheduler stops
 * routing that engine THERE — without ever forcing a job onto a worse factory.
 * The breaker only ever RESTRICTS the candidate set; it never overrides the
 * deterministic §7 scoring or forces a route.
 *
 * States mirror the sidecar breaker (extraction-service): CLOSED (normal) → OPEN
 * (skip-route) → HALF_OPEN (allow one probe after the reset window). A single
 * probe success closes the breaker; a probe failure re-opens it.
 *
 * This module is PURE w.r.t. I/O — state is in-memory and process-wide (one
 * coordinator process owns the claim path, matching the reaper-stats pattern).
 * Time is injectable so the logic is fully unit-testable without a real clock.
 */

type BreakerState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';

export interface EngineBreakerOptions {
  /** Consecutive failures on a (factory, engine) before the breaker OPENS. */
  failureThreshold?: number;
  /** How long a breaker stays OPEN before allowing a single HALF_OPEN probe. */
  resetTimeoutMs?: number;
}

/** One (factory, engine) entry — its current state plus a snapshot for operators. */
export interface EngineBreakerEntry {
  factoryId: string;
  engine: string;
  state: BreakerState;
  failureCount: number;
  lastFailureAt: string | null;
}

interface Cell {
  state: BreakerState;
  failureCount: number;
  lastFailureMs: number;
}

const DEFAULT_FAILURE_THRESHOLD = 3;
const DEFAULT_RESET_TIMEOUT_MS = 5 * 60_000; // 5 minutes

function key(factoryId: string, engine: string): string {
  return `${factoryId}\u0000${engine}`;
}

/**
 * A registry of per-`(factoryId, engine)` circuit breakers. Construct directly in
 * tests (with injected time); the coordinator uses the shared module singleton.
 */
export class EngineCircuitBreaker {
  private readonly cells = new Map<string, Cell>();
  private readonly failureThreshold: number;
  private readonly resetTimeoutMs: number;

  constructor(opts: EngineBreakerOptions = {}) {
    this.failureThreshold = opts.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
    this.resetTimeoutMs = opts.resetTimeoutMs ?? DEFAULT_RESET_TIMEOUT_MS;
  }

  /**
   * Whether a job pinned to `engine` may be routed to `factoryId` right now.
   * CLOSED ⇒ yes. OPEN ⇒ no, until the reset window elapses, after which we move
   * to HALF_OPEN and allow exactly one probe. May mutate state (OPEN→HALF_OPEN),
   * mirroring the sidecar breaker's `allowRequest`.
   */
  allow(factoryId: string, engine: string, now: number = Date.now()): boolean {
    const cell = this.cells.get(key(factoryId, engine));
    if (!cell || cell.state === 'CLOSED') return true;
    if (cell.state === 'OPEN') {
      if (now - cell.lastFailureMs >= this.resetTimeoutMs) {
        cell.state = 'HALF_OPEN';
        return true; // allow a single probe
      }
      return false;
    }
    return true; // HALF_OPEN — the probe is in flight; allow it through
  }

  /** Inverse of {@link allow} — convenience for "is this route blocked?". */
  isOpen(factoryId: string, engine: string, now: number = Date.now()): boolean {
    return !this.allow(factoryId, engine, now);
  }

  /** Record a failed run of `engine` on `factoryId`; may trip the breaker OPEN. */
  recordFailure(factoryId: string, engine: string, now: number = Date.now()): void {
    const k = key(factoryId, engine);
    const cell = this.cells.get(k) ?? { state: 'CLOSED', failureCount: 0, lastFailureMs: 0 };
    cell.failureCount += 1;
    cell.lastFailureMs = now;
    // A failed probe (HALF_OPEN) re-opens immediately; otherwise open on threshold.
    if (cell.state === 'HALF_OPEN' || cell.failureCount >= this.failureThreshold) {
      cell.state = 'OPEN';
    }
    this.cells.set(k, cell);
  }

  /** Record a successful run of `engine` on `factoryId` — closes/resets the pair. */
  recordSuccess(factoryId: string, engine: string): void {
    const k = key(factoryId, engine);
    const cell = this.cells.get(k);
    if (!cell) return; // never failed ⇒ stays implicitly CLOSED
    cell.state = 'CLOSED';
    cell.failureCount = 0;
    this.cells.set(k, cell);
  }

  /**
   * Operator snapshot of every tracked pair, with OPEN→HALF_OPEN reset applied as
   * of `now` so the view reflects what routing would actually do. Read-only:
   * callers get a copy and cannot mutate internal state.
   */
  snapshot(now: number = Date.now()): EngineBreakerEntry[] {
    const out: EngineBreakerEntry[] = [];
    for (const [k, cell] of this.cells) {
      const sep = k.indexOf('\u0000');
      const factoryId = k.slice(0, sep);
      const engine = k.slice(sep + 1);
      const open = cell.state === 'OPEN' && now - cell.lastFailureMs < this.resetTimeoutMs;
      out.push({
        factoryId,
        engine,
        state: open ? 'OPEN' : cell.state === 'OPEN' ? 'HALF_OPEN' : cell.state,
        failureCount: cell.failureCount,
        lastFailureAt: cell.lastFailureMs ? new Date(cell.lastFailureMs).toISOString() : null,
      });
    }
    return out;
  }

  /** Drop all state (tests / operator reset). */
  clear(): void {
    this.cells.clear();
  }
}

/** Process-wide breaker shared by the claim path + metrics (one coordinator). */
export const engineBreaker = new EngineCircuitBreaker();

/** Snapshot of the shared breaker for operator metrics / the fleet dashboard. */
export function getEngineBreakerSnapshot(now: number = Date.now()): EngineBreakerEntry[] {
  return engineBreaker.snapshot(now);
}

/**
 * `FLEET_ENGINE_BREAKER` env gate — default OFF. The breaker always RECORDS
 * outcomes (so its state is observable in metrics even before enforcement), but
 * only RESTRICTS routing when this flag is enabled. Keeps default behavior
 * byte-for-byte unchanged.
 */
export function isEngineBreakerEnabled(): boolean {
  const v = (process.env.FLEET_ENGINE_BREAKER ?? '').trim().toLowerCase();
  return v === '1' || v === 'true' || v === 'on' || v === 'yes';
}
