export interface RateLimiterOptions {
  burst: number;
  refillPerSec: number;
  maxQueueDepth?: number;
}

interface QueueEntry {
  tokens: number;
  resolve: () => void;
  reject: (error: Error) => void;
}

export class RateLimiter {
  private readonly capacity: number;
  private readonly refillPerMs: number;
  private readonly maxQueueDepth: number;
  private tokens: number;
  private lastRefill: number;
  private readonly queue: QueueEntry[] = [];
  private timer: NodeJS.Timeout | undefined;

  constructor(options: RateLimiterOptions) {
    if (!Number.isFinite(options.burst) || options.burst <= 0) {
      throw new Error('RateLimiter burst must be > 0');
    }
    if (!Number.isFinite(options.refillPerSec) || options.refillPerSec <= 0) {
      throw new Error('RateLimiter refillPerSec must be > 0');
    }
    this.capacity = options.burst;
    this.tokens = options.burst;
    this.refillPerMs = options.refillPerSec / 1000;
    this.maxQueueDepth = options.maxQueueDepth ?? 100;
    this.lastRefill = Date.now();
  }

  acquire(tokens = 1): Promise<void> {
    if (!Number.isFinite(tokens) || tokens <= 0) {
      return Promise.resolve();
    }
    return new Promise((resolve, reject) => {
      if (this.queue.length >= this.maxQueueDepth) {
        reject(new Error('RateLimiter queue exceeded'));
        return;
      }
      this.queue.push({ tokens, resolve, reject });
      this.processQueue();
    });
  }

  tryAcquire(tokens = 1): boolean {
    this.refillTokens();
    if (this.tokens >= tokens) {
      this.tokens -= tokens;
      return true;
    }
    return false;
  }

  pending(): number {
    return this.queue.length;
  }

  stop(): void {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = undefined;
    }
    this.queue.splice(0, this.queue.length).forEach(entry => entry.reject(new Error('RateLimiter stopped')));
  }

  private processQueue(): void {
    this.refillTokens();

    while (this.queue.length > 0) {
      const entry = this.queue[0];
      if (this.tokens >= entry.tokens) {
        this.tokens -= entry.tokens;
        this.queue.shift();
        entry.resolve();
      } else {
        break;
      }
    }

    if (this.queue.length === 0) {
      if (this.timer) {
        clearTimeout(this.timer);
        this.timer = undefined;
      }
      return;
    }

    if (!this.timer) {
      const entry = this.queue[0];
      const needed = entry.tokens - this.tokens;
      const wait = Math.max(50, needed / this.refillPerMs);
      this.timer = setTimeout(() => {
        this.timer = undefined;
        this.processQueue();
      }, wait);
    }
  }

  private refillTokens(): void {
    const now = Date.now();
    if (now <= this.lastRefill) return;
    const delta = now - this.lastRefill;
    const refill = delta * this.refillPerMs;
    if (refill > 0) {
      this.tokens = Math.min(this.capacity, this.tokens + refill);
      this.lastRefill = now;
    }
  }
}
