/// Controls the rate at which API requests are issued.
///
/// Implementations can enforce per-second, per-minute, or token-bucket limits
/// to stay within the quotas imposed by external APIs.
abstract class RateLimiter {
  /// Waits until a request token is available, then returns.
  ///
  /// Callers must await [acquire] before issuing each request.
  Future<void> acquire();

  /// Releases any resources held by this limiter (timers, streams, etc.).
  void dispose();
}

/// A [RateLimiter] that imposes no rate limit.
///
/// Suitable for development, testing, or APIs with no documented quota.
class NoRateLimiter implements RateLimiter {
  const NoRateLimiter();

  @override
  Future<void> acquire() async {}

  @override
  void dispose() {}
}

/// A simple token-bucket [RateLimiter] that allows at most [maxRequests]
/// requests per [window].
///
/// Requests that arrive when the bucket is empty are queued and released
/// as tokens refill at the start of each window.
class TokenBucketRateLimiter implements RateLimiter {
  /// Maximum number of requests allowed per [window].
  final int maxRequests;

  /// The time window over which [maxRequests] are allowed.
  final Duration window;

  int _tokens;
  DateTime _windowStart;

  TokenBucketRateLimiter({required this.maxRequests, this.window = const Duration(seconds: 1)})
    : _tokens = maxRequests,
      _windowStart = DateTime.now();

  @override
  Future<void> acquire() async {
    final now = DateTime.now();
    if (now.difference(_windowStart) >= window) {
      _tokens = maxRequests;
      _windowStart = now;
    }

    if (_tokens > 0) {
      _tokens--;
      return;
    }

    // Wait for the remainder of the current window, then retry.
    final remaining = window - now.difference(_windowStart);
    await Future<void>.delayed(remaining);
    return acquire();
  }

  @override
  void dispose() {}
}
