import { createDbService } from '@/server/services/db.js';
/**
 * Request-level rate limiting via the `rate_limit_buckets` table.
 *
 * Design goals:
 * - Atomic conditional UPDATE eliminates the TOCTOU race (no select-then-update).
 * - IP is SHA-256 hashed — raw IPs are never stored or logged.
 * - Fail open: if IP extraction or hashing throws, request is allowed through
 *   and the error is reported to Sentry.
 *
 * Usage:
 *   const tooMany = await applyRateLimitFor(request, 'auth.login');
 *   if (tooMany) return tooMany; // 429 Response
 */

import { consumeRateLimit } from '@/server/db/queries/rate-limits.js';
import { env } from '@/server/env.js';
import { getIp, hashIp } from './ip.js';
import { getRateLimitPolicy, type RateLimitScope } from './rate-limit-policies.js';
import { captureCaught } from '@/server/observability/capture.server';

/**
 * Low-level rate-limit check.
 *
 * @param request    The incoming Request (used to extract IP).
 * @param keyBase    Stable identifier for the endpoint / action.
 * @param max        Maximum allowed requests in the window.
 * @param windowMs   Window duration in milliseconds.
 * @param subjectKey Optional authenticated subject ID — uses user key instead of IP key.
 * @returns 429 Response when limit exceeded, or `null` when allowed through.
 */
export async function applyRateLimit(
  request: Request,
  keyBase: string,
  max: number,
  windowMs: number,
  subjectKey?: string,
): Promise<Response | null> {
  let subject = subjectKey;
  if (!subject) {
    try {
      const ip = getIp(request);
      subject = await hashIp(ip);
    } catch (err) {
      captureCaught(err, { scope: 'server.security.rate-limit', severity: 'warning' });
      return null;
    }
  }

  const bucketKey = `${keyBase}:${subject}`;
  const now = new Date();
  const windowEnd = new Date(now.getTime() + windowMs);

  const db = createDbService({ DATABASE_URL: env.DATABASE_URL });

  let bucket: { count: number; refilledAt: Date } | null;
  try {
    bucket = await consumeRateLimit(db, bucketKey, now, windowEnd);
  } catch (err) {
    captureCaught(err, { scope: 'server.security.rate-limit', severity: 'warning' });
    return null;
  }

  if (!bucket) return null;

  if (bucket.count > max) {
    const retryAfter = Math.max(0, Math.ceil((bucket.refilledAt.getTime() - now.getTime()) / 1000));
    return new Response(
      JSON.stringify({ ok: false, error: 'Too many requests', code: 'RATE_LIMITED' }),
      {
        status: 429,
        headers: {
          'Content-Type': 'application/json',
          'Retry-After': String(retryAfter),
          'X-RateLimit-Limit': String(max),
          'X-RateLimit-Remaining': '0',
          'X-RateLimit-Reset': String(Math.floor(bucket.refilledAt.getTime() / 1000)),
        },
      },
    );
  }

  return null;
}

/**
 * Policy-scoped rate-limit check. Preferred over calling `applyRateLimit` directly.
 *
 * @param request    The incoming request.
 * @param scope      One of the registered rate-limit scopes (see rate-limit-policies.ts).
 * @param subjectKey Optional authenticated subject ID.
 * @returns 429 Response when limit exceeded, or `null` when allowed through.
 */
export async function applyRateLimitFor(
  request: Request,
  scope: RateLimitScope,
  subjectKey?: string,
): Promise<Response | null> {
  const { max, windowMs } = getRateLimitPolicy(scope);
  return applyRateLimit(request, scope, max, windowMs, subjectKey);
}
