/**
 * Daily AI-usage caps for the scan route (in-memory).
 *
 * ## What is counted
 * AI CALLS, not raw pages: a multi-page PDF is merged into one vision job and
 * consumes 1 unit of the daily budget. The route records 1 AFTER extraction;
 * a cheap pre-check (`checkDailyUsage`) rejects requests once the budget is
 * exhausted without paying for a potentially expensive parse first.
 *
 * ## Window semantics
 * A rolling fixed window of 24h: the first `recordUsage` for a key starts the
 * window, every later record within it increments the counter, and the window
 * rolls over at `resetAt`. `checkDailyUsage` never mutates state — it reads
 * the live bucket (or reports zero usage when none exists yet).
 *
 * ## Overshoot caveat (documented behaviour)
 * Because the pre-check only sees the budget BEFORE extraction and the record
 * happens AFTER it, parallel requests can push the counter slightly past the
 * limit (each of N concurrent uploads records 1 after all pre-checks passed).
 * That overshoot is bounded by upload concurrency and is acceptable for an
 * abuse cap. The monthly database quota remains the hard billing controller.
 *
 * ## Deployment caveat (read before scaling out)
 * The counters live in the process heap, so each server instance enforces its
 * OWN budget: N replicas behind a load balancer multiply the effective cap by
 * N and a restart clears every bucket. Acceptable for a single-instance
 * deployment; before scaling out, move the counters to a shared store
 * (Redis INCR+EXPIRE or a Postgres table) keyed by the same
 * `ai:user:<id>` / `ai:guest:<bucket>` strings.
 */

const DAILY_WINDOW_MS = 24 * 60 * 60 * 1000;

/**
 * Default in-memory remaining for `ai:user:` keys. Not a product cap:
 * Free is limited by the monthly quota (15 / 100 launch bonus).
 */
export const DAILY_SCAN_LIMIT = 30;

/** Default remaining for leftover `ai:guest:` keys. Scan rejects unsigned callers. */
export const DAILY_GUEST_SCAN_LIMIT = 10;

/**
 * Paid Pro users: AI-call units per rolling 24h.
 * Sized so a 150+/month catch-up dump fits in one sitting; still a hard
 * ceiling against scripted drain of the vision budget.
 */
export const DAILY_PRO_SCAN_LIMIT = 150;

/**
 * Daily product cap. Free has none (`null`) — the monthly DB quota is the
 * limiter. Pro gets {@link DAILY_PRO_SCAN_LIMIT}.
 */
export function dailyScanLimitFor(opts: { isPro: boolean }): number | null {
  return opts.isPro ? DAILY_PRO_SCAN_LIMIT : null;
}

/**
 * Short-window guard (informational). The per-user 120 requests / 60 min
 * window enforced in the scan route (`scan:user:<id>`) remains the fast
 * flood-control layer; this daily cap protects the AI budget specifically.
 */
export const HOURLY_SCAN_LIMIT = 120;

const USER_KEY_PREFIX = "ai:user:";
const GUEST_KEY_PREFIX = "ai:guest:";

/** Per-key budget lookup so `remaining` is meaningful for both key kinds. */
function limitForKey(key: string): number {
  return key.startsWith(GUEST_KEY_PREFIX) ? DAILY_GUEST_SCAN_LIMIT : DAILY_SCAN_LIMIT;
}

interface UsageBucket {
  count: number;
  resetAt: number;
}

/** Hard ceiling on tracked keys so a buggy caller cannot grow the map unbounded. */
const MAX_BUCKETS = 10_000;

const buckets = new Map<string, UsageBucket>();

/** Opportunistic eviction of expired buckets once the map gets large. */
function sweep(now: number): void {
  if (buckets.size < MAX_BUCKETS) return;
  for (const [key, bucket] of buckets) {
    if (bucket.resetAt <= now) buckets.delete(key);
  }
}

export interface DailyUsageCheck {
  /** True while at least one unit of budget remains (`used < limit`). */
  allowed: boolean;
  /** Units consumed in the current 24h window. */
  used: number;
  /** `limit - used`, clamped at 0. */
  remaining: number;
  /** Epoch ms when the window resets — surface as `Retry-After` (seconds). */
  resetsAt: number;
}

/**
 * Reads the current daily budget for `key` without mutating state. Used as the
 * pre-extraction fast 429 gate. `now` is injectable for deterministic tests;
 * `limit` defaults to the key-derived cap (`ai:guest:` → DAILY_GUEST_SCAN_LIMIT,
 * anything else → DAILY_SCAN_LIMIT). Malformed input is treated as "allow" so a
 * bad configuration can never lock out real users.
 */
export function checkDailyUsage(
  key: string,
  limit: number = limitForKey(key),
  now: Date = new Date()
): DailyUsageCheck {
  const safeLimit = Number.isFinite(limit) && limit >= 1 ? Math.floor(limit) : limitForKey(key);
  const nowMs = now.getTime();
  sweep(nowMs);

  if (!key) {
    return { allowed: true, used: 0, remaining: safeLimit, resetsAt: nowMs + DAILY_WINDOW_MS };
  }

  const bucket = buckets.get(key);
  const live = !!bucket && bucket.resetAt > nowMs;
  const used = live ? bucket.count : 0;
  return {
    allowed: used < safeLimit,
    used,
    remaining: Math.max(0, safeLimit - used),
    resetsAt: live ? bucket.resetAt : nowMs + DAILY_WINDOW_MS,
  };
}

/**
 * Adds `units` (AI calls / pages) to the counter for `key`. The first record in
 * a window starts it (`resetAt = now + 24h`); subsequent records increment it.
 * Non-finite or non-positive units are normalized to 1 so a caller can never
 * under-count. `now` is injectable for deterministic tests.
 */
export function recordUsage(
  key: string,
  units: number,
  now: Date = new Date()
): { used: number; remaining: number } {
  const nowMs = now.getTime();
  const safeUnits = Number.isFinite(units) && units > 0 ? Math.max(1, Math.ceil(units)) : 1;
  const limit = limitForKey(key);

  if (!key) {
    return { used: 0, remaining: limit };
  }

  sweep(nowMs);

  const bucket = buckets.get(key);
  if (!bucket || bucket.resetAt <= nowMs) {
    buckets.set(key, { count: safeUnits, resetAt: nowMs + DAILY_WINDOW_MS });
    return { used: safeUnits, remaining: Math.max(0, limit - safeUnits) };
  }

  bucket.count += safeUnits;
  return { used: bucket.count, remaining: Math.max(0, limit - bucket.count) };
}

/** Canonical daily-cap key for a signed-in user. */
export function usageKeyForUser(userId: string): string {
  return USER_KEY_PREFIX + userId;
}

/** Canonical daily-cap key for a guest bucket (see `src/lib/auth/guest.ts`). */
export function usageKeyForGuest(bucket: string): string {
  return GUEST_KEY_PREFIX + bucket;
}
