// In-memory token bucket rate limiter keyed by an arbitrary id (user id).
//
// Token bucket semantics:
//   - Capacity = `burst` (max tokens the bucket can hold).
//   - Refill   = `perMinute / 60` tokens per second (lazy: tokens are added
//                on `consume` based on elapsed time since the bucket was
//                last touched).
//   - Each accepted call consumes exactly one token. Calls that find the
//     bucket empty are rejected and the caller gets back a Retry-After hint
//     in milliseconds.
//
// Stale entries: the map only grows by the number of distinct ids seen.
// For a 5-user MVP this is bounded; for larger installs the caller can call
// `pruneStale(maxIdleMs)` periodically.

import { envNumber } from "./env.js";

export type RateLimiterOptions = {
  perMinute: number;
  burst: number;
};

export type ConsumeResult =
  | { ok: true; remaining: number }
  | { ok: false; retryAfterMs: number };

export type RateLimiter = {
  consume(id: string, now?: number): ConsumeResult;
  size: () => number;
  reset: (id?: string) => void;
};

export const createRateLimiter = (
  options: RateLimiterOptions,
): RateLimiter => {
  const { perMinute, burst } = options;
  const refillPerMs = perMinute / 60_000;
  const buckets = new Map<string, { tokens: number; lastRefillMs: number }>();

  const consume = (id: string, now: number = Date.now()): ConsumeResult => {
    let bucket = buckets.get(id);
    if (!bucket) {
      bucket = { tokens: burst, lastRefillMs: now };
      buckets.set(id, bucket);
    } else {
      const elapsed = Math.max(0, now - bucket.lastRefillMs);
      const refilled = elapsed * refillPerMs;
      if (refilled > 0) {
        bucket.tokens = Math.min(burst, bucket.tokens + refilled);
        bucket.lastRefillMs = now;
      }
    }

    if (bucket.tokens >= 1) {
      bucket.tokens -= 1;
      return { ok: true, remaining: Math.floor(bucket.tokens) };
    }

    // Time until the bucket has at least 1 token.
    const needed = 1 - bucket.tokens;
    const retryAfterMs = refillPerMs > 0 ? Math.ceil(needed / refillPerMs) : 60_000;
    return { ok: false, retryAfterMs };
  };

  return {
    consume,
    size: () => buckets.size,
    reset: (id) => {
      if (id === undefined) buckets.clear();
      else buckets.delete(id);
    },
  };
};

export const chatRateLimiterFromEnv = (): RateLimiter => {
  const perMinute = Math.max(1, envNumber(process.env.CHAT_RATE_LIMIT_PER_MINUTE, 20));
  const burst = Math.max(1, envNumber(process.env.CHAT_RATE_LIMIT_BURST, 5));
  return createRateLimiter({ perMinute, burst });
};

export const webhookRateLimiterFromEnv = (): RateLimiter => {
  // Defaults: 60/min refill, burst 10. Generous on purpose — the goal is
  // to stop a runaway loop, not throttle a real operator. Tighten per
  // webhook_id in the future if a specific hook becomes a hotspot.
  const perMinute = Math.max(1, envNumber(process.env.WEBHOOK_RATE_LIMIT_PER_MINUTE, 60));
  const burst = Math.max(1, envNumber(process.env.WEBHOOK_RATE_LIMIT_BURST, 10));
  return createRateLimiter({ perMinute, burst });
};
