export type Duration = number | `${number}${"ms" | "s" | "m" | "h"}`;

export type BackoffStrategy = "fixed" | "exponential" | ((attempt: number) => Duration);

export interface CircuitBreakerOptions {
  failures: number;
  resetAfter: Duration;
  successesToClose?: number;
}

export interface CircuitBreakerSnapshot {
  state: "closed" | "open" | "half-open";
  failures: number;
  successes: number;
  retryAfterMs: number;
}

export class ResilienceError extends Error {
  constructor(
    public readonly code:
      | "WRN-RESILIENCE-TIMEOUT"
      | "WRN-RESILIENCE-ABORTED"
      | "WRN-RESILIENCE-CIRCUIT-OPEN"
      | "WRN-RESILIENCE-BULKHEAD-FULL",
    message: string,
    options?: ErrorOptions,
  ) {
    super(message, options);
    this.name = "ResilienceError";
  }
}

export function durationMs(value: Duration): number {
  if (typeof value === "number") {
    if (!Number.isFinite(value) || value < 0)
      throw new TypeError("Duration must be finite and non-negative.");
    return value;
  }
  const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/.exec(value);
  if (!match) throw new TypeError(`Invalid duration: ${value}`);
  const scale = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000 }[match[2]!]!;
  return Number(match[1]) * scale;
}

export class CircuitBreaker {
  private failures = 0;
  private successes = 0;
  private openedAt = 0;
  private probing = false;

  constructor(private readonly options: CircuitBreakerOptions) {
    if (!Number.isInteger(options.failures) || options.failures < 1) {
      throw new TypeError("Circuit breaker failures must be a positive integer.");
    }
    durationMs(options.resetAfter);
  }

  snapshot(now = Date.now()): CircuitBreakerSnapshot {
    const resetAfter = durationMs(this.options.resetAfter);
    const elapsed = now - this.openedAt;
    const open = this.openedAt > 0 && elapsed < resetAfter;
    return {
      state: open ? "open" : this.openedAt > 0 ? "half-open" : "closed",
      failures: this.failures,
      successes: this.successes,
      retryAfterMs: open ? Math.max(0, resetAfter - elapsed) : 0,
    };
  }

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    const health = this.snapshot();
    if (health.state === "open" || (health.state === "half-open" && this.probing)) {
      throw new ResilienceError(
        "WRN-RESILIENCE-CIRCUIT-OPEN",
        `Circuit is open; retry after ${health.retryAfterMs}ms.`,
      );
    }
    if (health.state === "half-open") this.probing = true;
    try {
      const value = await operation();
      this.failures = 0;
      this.successes += 1;
      if (this.successes >= (this.options.successesToClose ?? 1)) this.openedAt = 0;
      return value;
    } catch (error) {
      this.successes = 0;
      this.failures += 1;
      if (this.failures >= this.options.failures) this.openedAt = Date.now();
      throw error;
    } finally {
      this.probing = false;
    }
  }
}

export interface BulkheadOptions {
  concurrency: number;
  queue?: number;
}

export class Bulkhead {
  private active = 0;
  private readonly waiting: Array<() => void> = [];

  constructor(private readonly options: BulkheadOptions) {
    if (!Number.isInteger(options.concurrency) || options.concurrency < 1) {
      throw new TypeError("Bulkhead concurrency must be a positive integer.");
    }
    if (options.queue !== undefined && (!Number.isInteger(options.queue) || options.queue < 0)) {
      throw new TypeError("Bulkhead queue must be a non-negative integer.");
    }
  }

  get snapshot(): Readonly<{ active: number; queued: number; capacity: number }> {
    return { active: this.active, queued: this.waiting.length, capacity: this.options.concurrency };
  }

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.active >= this.options.concurrency) {
      if (this.waiting.length >= (this.options.queue ?? 0)) {
        throw new ResilienceError(
          "WRN-RESILIENCE-BULKHEAD-FULL",
          "Bulkhead capacity is exhausted.",
        );
      }
      await new Promise<void>((resolve) => this.waiting.push(resolve));
    }
    this.active += 1;
    try {
      return await operation();
    } finally {
      this.active -= 1;
      this.waiting.shift()?.();
    }
  }
}

export interface ResilientCallOptions<T> {
  run: (signal: AbortSignal, attempt: number) => Promise<T>;
  timeout?: Duration;
  retries?: number;
  retryDelay?: Duration;
  backoff?: BackoffStrategy;
  circuitBreaker?: CircuitBreaker | CircuitBreakerOptions;
  bulkhead?: Bulkhead | BulkheadOptions;
  signal?: AbortSignal;
  retryWhen?: (error: unknown, attempt: number) => boolean | Promise<boolean>;
  fallback?: (error: unknown, signal: AbortSignal) => T | Promise<T>;
  onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
}

const breakerInstances = new WeakMap<CircuitBreakerOptions, CircuitBreaker>();
const bulkheadInstances = new WeakMap<BulkheadOptions, Bulkhead>();

function breakerFor(value: CircuitBreaker | CircuitBreakerOptions): CircuitBreaker {
  if (value instanceof CircuitBreaker) return value;
  const existing = breakerInstances.get(value);
  if (existing) return existing;
  const created = new CircuitBreaker(value);
  breakerInstances.set(value, created);
  return created;
}

function bulkheadFor(value: Bulkhead | BulkheadOptions): Bulkhead {
  if (value instanceof Bulkhead) return value;
  const existing = bulkheadInstances.get(value);
  if (existing) return existing;
  const created = new Bulkhead(value);
  bulkheadInstances.set(value, created);
  return created;
}

async function abortable<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
  if (signal.aborted) throw signal.reason;
  let cleanup = () => {};
  const aborted = new Promise<never>((_resolve, reject) => {
    const listener = () => reject(signal.reason);
    signal.addEventListener("abort", listener, { once: true });
    cleanup = () => signal.removeEventListener("abort", listener);
  });
  try {
    return await Promise.race([operation, aborted]);
  } finally {
    cleanup();
  }
}

function abortError(signal: AbortSignal): ResilienceError {
  return new ResilienceError("WRN-RESILIENCE-ABORTED", "Resilient call was aborted.", {
    cause: signal.reason,
  });
}

async function delay(ms: number, signal?: AbortSignal): Promise<void> {
  if (signal?.aborted) throw abortError(signal);
  await new Promise<void>((resolve, reject) => {
    const timer = setTimeout(resolve, ms);
    signal?.addEventListener(
      "abort",
      () => {
        clearTimeout(timer);
        reject(abortError(signal));
      },
      { once: true },
    );
  });
}

export async function resilientCall<T>(options: ResilientCallOptions<T>): Promise<T> {
  const retries = options.retries ?? 0;
  if (!Number.isInteger(retries) || retries < 0)
    throw new TypeError("Retries must be a non-negative integer.");
  const breaker = options.circuitBreaker ? breakerFor(options.circuitBreaker) : undefined;
  const bulkhead = options.bulkhead ? bulkheadFor(options.bulkhead) : undefined;
  const invoke = async (): Promise<T> => {
    let lastError: unknown;
    for (let attempt = 1; attempt <= retries + 1; attempt += 1) {
      if (options.signal?.aborted) throw abortError(options.signal);
      const controller = new AbortController();
      const forwardAbort = () => controller.abort(options.signal?.reason);
      options.signal?.addEventListener("abort", forwardAbort, { once: true });
      const timeout = options.timeout === undefined ? undefined : durationMs(options.timeout);
      const timer =
        timeout === undefined ? undefined : setTimeout(() => controller.abort("timeout"), timeout);
      try {
        const run = () => abortable(options.run(controller.signal, attempt), controller.signal);
        return await (breaker ? breaker.execute(run) : run());
      } catch (caught) {
        lastError =
          controller.signal.aborted && !options.signal?.aborted
            ? new ResilienceError(
                "WRN-RESILIENCE-TIMEOUT",
                `Operation timed out after ${timeout}ms.`,
                { cause: caught },
              )
            : caught;
        if (attempt > retries || !(await (options.retryWhen?.(lastError, attempt) ?? true))) break;
        const base = durationMs(options.retryDelay ?? 100);
        const wait =
          typeof options.backoff === "function"
            ? durationMs(options.backoff(attempt))
            : options.backoff === "exponential"
              ? base * 2 ** (attempt - 1)
              : base;
        options.onRetry?.(lastError, attempt, wait);
        await delay(wait, options.signal);
      } finally {
        if (timer !== undefined) clearTimeout(timer);
        options.signal?.removeEventListener("abort", forwardAbort);
      }
    }
    if (options.fallback)
      return options.fallback(lastError, options.signal ?? new AbortController().signal);
    throw lastError;
  };
  return bulkhead ? bulkhead.execute(invoke) : invoke();
}
