// ---------------------------------------------------------------------------
// MikroMCP - Circuit breaker for router connections
// ---------------------------------------------------------------------------

import { MikroMCPError, ErrorCategory } from "../domain/errors/error-types.js";

export type CircuitState = "closed" | "open" | "half-open";

const TRANSIENT_CATEGORIES = new Set([
  ErrorCategory.ROUTER_UNREACHABLE,
  ErrorCategory.ROUTER_TIMEOUT,
  ErrorCategory.ROUTER_ERROR,
  ErrorCategory.ROUTER_BUSY,
]);

function isTransientFailure(error: unknown): boolean {
  if (!(error instanceof MikroMCPError)) {
    return false;
  }
  return TRANSIENT_CATEGORIES.has(error.category);
}

export interface CircuitBreakerOptions {
  /** Number of consecutive failures before opening the circuit (default 5). */
  failureThreshold: number;
  /** Time in ms to wait before transitioning from open to half-open (default 30000). */
  cooldownMs: number;
}

const DEFAULTS: CircuitBreakerOptions = {
  failureThreshold: 5,
  cooldownMs: 30_000,
};

export class CircuitBreaker {
  private readonly options: CircuitBreakerOptions;
  private currentState: CircuitState = "closed";
  private consecutiveFailures = 0;
  private lastFailureTime = 0;
  private probeInFlight = false;

  constructor(
    private readonly routerId: string,
    options?: Partial<CircuitBreakerOptions>,
  ) {
    this.options = { ...DEFAULTS, ...options };
  }

  /** Effective state, accounting for an elapsed cooldown. Pure — does not mutate. */
  get state(): CircuitState {
    if (this.currentState === "open" && this.cooldownElapsed()) {
      return "half-open";
    }
    return this.currentState;
  }

  private cooldownElapsed(): boolean {
    return Date.now() - this.lastFailureTime >= this.options.cooldownMs;
  }

  /** Apply the time-based open→half-open transition. Call before acting on state. */
  private transitionIfCooldownElapsed(): void {
    if (this.currentState === "open" && this.cooldownElapsed()) {
      this.currentState = "half-open";
    }
  }

  /**
   * Execute `fn` through the circuit breaker.
   *
   * - **closed**: execute normally; track consecutive failures.
   * - **open**: reject immediately with `ROUTER_BUSY`.
   * - **half-open**: execute as a probe; success closes, failure reopens.
   */
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    this.transitionIfCooldownElapsed();
    const currentState = this.currentState;

    if (currentState === "open") {
      throw new MikroMCPError({
        category: ErrorCategory.ROUTER_BUSY,
        code: "CIRCUIT_OPEN",
        message: `Circuit breaker is open for router "${this.routerId}". Try again after cooldown.`,
        details: {
          routerId: this.routerId,
          consecutiveFailures: this.consecutiveFailures,
          cooldownMs: this.options.cooldownMs,
        },
        recoverability: {
          retryable: true,
          retryAfterMs: this.options.cooldownMs,
          suggestedAction: "Wait for the circuit breaker cooldown period to expire, then retry.",
        },
      });
    }

    if (currentState === "half-open") {
      if (this.probeInFlight) {
        throw new MikroMCPError({
          category: ErrorCategory.ROUTER_BUSY,
          code: "CIRCUIT_PROBE_IN_FLIGHT",
          message: `Circuit breaker for router "${this.routerId}" is half-open and a recovery probe is already in flight.`,
          details: { routerId: this.routerId },
          recoverability: {
            retryable: true,
            retryAfterMs: 1000,
            suggestedAction: "Wait for the in-flight probe to complete, then retry.",
          },
        });
      }
      this.probeInFlight = true;
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      if (isTransientFailure(error)) {
        this.onFailure();
      }
      throw error;
    } finally {
      // Safe to clear unconditionally: the half-open gate sets the flag only
      // after its early-return check, so non-half-open paths never set it.
      this.probeInFlight = false;
    }
  }

  /** Force the circuit back to the closed state. */
  reset(): void {
    this.currentState = "closed";
    this.consecutiveFailures = 0;
    this.lastFailureTime = 0;
    this.probeInFlight = false;
  }

  // ---------- internal helpers ----------

  private onSuccess(): void {
    this.consecutiveFailures = 0;
    this.currentState = "closed";
  }

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

    if (this.currentState === "half-open") {
      // Probe failed - reopen.
      this.currentState = "open";
    } else if (this.consecutiveFailures >= this.options.failureThreshold) {
      this.currentState = "open";
    }
  }
}
