/**
 * Input for a single rate-limit hit.
 */
export interface RateLimitHitOptions {
  /**
   * Unique key for this rate limit window.
   *
   * Examples: "global", "ip:203.0.113.10", "user:123".
   */
  key: string;
  /**
   * Maximum allowed hits inside the window.
   */
  limit: number;
  /**
   * Length of the window in seconds.
   */
  windowSec: number;
}

/**
 * Result of recording a rate-limit hit.
 */
export interface RateLimitResult {
  /**
   * True when the hit is within the configured limit.
   */
  allowed: boolean;
  /**
   * Remaining allowed hits in the window, if known. May be null if the
   * implementation does not track it.
   */
  remaining: number | null;
  /**
   * Date when the window resets, if known. May be null.
   */
  resetAt: Date | null;
  /**
   * Seconds until the caller should retry, if the hit was rejected and the
   * implementation can calculate it.
   */
  retryAfterSeconds: number | null;
}

/**
 * App-facing rate limiting port.
 *
 * Implement this with an atomic shared store such as Redis for production.
 * Hook helpers call `hit(...)` to decide whether a request should continue.
 */
export interface RateLimitPort {
  /**
   * Record one hit for a rate-limit key and return the current decision.
   */
  hit(options: RateLimitHitOptions): Promise<RateLimitResult>;
}

type MemoryRateLimitWindow = {
  count: number;
  resetAt: number;
};

function assertPositiveInteger(name: string, value: number): void {
  if (!Number.isInteger(value) || value <= 0) {
    throw new Error(`${name} must be a positive integer`);
  }
}

function toRetryAfterSeconds(resetAt: number): number {
  return Math.max(0, Math.ceil((resetAt - Date.now()) / 1000));
}

/**
 * Create an in-memory rate limiter for tests, examples, and single-process
 * development.
 *
 * This adapter is not durable or distributed. Production apps should use a
 * provider backed by a shared atomic store when multiple processes or regions
 * can serve requests.
 *
 * @returns A rate-limit port backed by a local `Map`.
 */
export function createMemoryRateLimiter(): RateLimitPort {
  const windows = new Map<string, MemoryRateLimitWindow>();
  const sweepIntervalMs = 60_000;
  let nextSweepAt = Date.now() + sweepIntervalMs;

  return {
    async hit({ key, limit, windowSec }) {
      assertPositiveInteger("limit", limit);
      assertPositiveInteger("windowSec", windowSec);

      const now = Date.now();
      // Lazy sweep: distinct keys would otherwise accumulate expired windows
      // forever. Prune them at most once per sweep interval on the hit path.
      if (now >= nextSweepAt) {
        for (const [existingKey, existing] of windows) {
          if (existing.resetAt <= now) {
            windows.delete(existingKey);
          }
        }
        nextSweepAt = now + sweepIntervalMs;
      }
      const current = windows.get(key);
      const window =
        current && current.resetAt > now
          ? current
          : {
              count: 0,
              resetAt: now + windowSec * 1000,
            };

      window.count += 1;
      windows.set(key, window);

      const allowed = window.count <= limit;
      const remaining = Math.max(0, limit - window.count);
      const resetAt = new Date(window.resetAt);

      return {
        allowed,
        remaining,
        resetAt,
        retryAfterSeconds: allowed ? null : toRetryAfterSeconds(window.resetAt),
      };
    },
  };
}
