import { jsonError } from './http.js';

// CF-native Rate Limiting binding (GA Sep 2025). Minimal STRUCTURAL shape so this helper stays
// unit-testable with a fake and does not depend on `@cloudflare/workers-types`' `RateLimit` being
// in lexical scope here.
export interface RateLimiter {
  limit(options: { key: string }): Promise<{ success: boolean }>;
}

export interface AdminLimiterEnv {
  ADMIN_LOGIN_LIMITER?: RateLimiter;
  ADMIN_API_LIMITER?: RateLimiter;
}

export interface CommentLimiterEnv {
  COMMENTS_LIMITER?: RateLimiter;
}

export interface FormLimiterEnv {
  FORMS_LIMITER?: RateLimiter;
}

const ADMIN_PREFIX = '/api/admin/';
const LOGIN_PATH = '/api/admin/session';

// Edge rate-limit for the ENTIRE admin surface, enforced once in middleware so any NEW `/api/admin/*`
// route is covered automatically — no per-endpoint wiring to forget (the same footgun class as a
// route leaking unguarded). Returns a 429 `Response` when the caller is over budget, else `null` to
// let the request proceed.
//
// Two buckets, both keyed by Cloudflare's client IP:
//   - login POST  -> tight bucket: brute-force / credential-stuffing defense, layered ON TOP of the
//     auth engine's own per-account credential throttle (defense in depth — IP-wide, not per-account).
//   - everything else under /api/admin/ -> generous abuse ceiling for authenticated agents/humans.
//
// Fail-open ONLY on binding ABSENCE (local dev / a preview without the namespace): rate-limiting is a
// defense-in-depth layer, and the auth gate + engine throttle still bind. It never throws on the hot path.
export async function enforceAdminRateLimit(
  env: AdminLimiterEnv,
  request: Request,
): Promise<Response | null> {
  const { pathname } = new URL(request.url);
  if (!pathname.startsWith(ADMIN_PREFIX)) return null;

  const isLogin = pathname === LOGIN_PATH && request.method === 'POST';
  const limiter = isLogin ? env.ADMIN_LOGIN_LIMITER : env.ADMIN_API_LIMITER;
  if (!limiter) return null;

  // CF-Connecting-IP is injected by Cloudflare on every edge request. A missing header (only when
  // run off-edge) degrades to a single shared bucket rather than throwing — safe, never a crash.
  const ip = request.headers.get('CF-Connecting-IP') ?? 'unknown';
  const key = `${isLogin ? 'login' : 'api'}:${ip}`;

  const { success } = await limiter.limit({ key });
  if (success) return null;

  const response = jsonError(429, 'rate_limited', 'Too many requests. Please retry shortly.');
  response.headers.set('Retry-After', '60');
  return response;
}

/** Public comment POST rate-limit — keyed comment:<ip>; fail-open when binding absent. */
export async function enforcePublicCommentRateLimit(
  env: CommentLimiterEnv,
  request: Request,
): Promise<Response | null> {
  const limiter = env.COMMENTS_LIMITER;
  if (!limiter) return null;

  const ip = request.headers.get('CF-Connecting-IP') ?? 'unknown';
  const { success } = await limiter.limit({ key: `comment:${ip}` });
  if (success) return null;

  const response = jsonError(429, 'rate_limited', 'Too many requests. Please retry shortly.');
  response.headers.set('Retry-After', '60');
  return response;
}

/** Public contact form POST rate-limit — keyed form:<ip>; fail-open when binding absent. */
export async function enforcePublicFormRateLimit(
  env: FormLimiterEnv,
  request: Request,
): Promise<Response | null> {
  const limiter = env.FORMS_LIMITER;
  if (!limiter) return null;

  const ip = request.headers.get('CF-Connecting-IP') ?? 'unknown';
  const { success } = await limiter.limit({ key: `form:${ip}` });
  if (success) return null;

  const response = jsonError(429, 'rate_limited', 'Too many requests. Please retry shortly.');
  response.headers.set('Retry-After', '60');
  return response;
}
