import { NextRequest, NextResponse } from 'next/server';
import { z, ZodSchema } from 'zod';

import {
  checkRateLimit,
  rateLimiters,
  rateLimitHeaders,
  type RateLimiterName,
} from '@/lib/rate-limit';

/**
 * Parses URL search params against a Zod schema.
 * Throws a ZodError on validation failure (caught by `errorResponse`).
 */
export function parseQuery<T extends ZodSchema>(req: NextRequest, schema: T): z.infer<T> {
  const params = Object.fromEntries(req.nextUrl.searchParams.entries());
  return schema.parse(params);
}

/**
 * Parses the JSON request body against a Zod schema.
 * Throws a ZodError on validation failure (caught by `errorResponse`).
 *
 * H-14: tolerates empty request bodies (content-length 0 or req.json()
 * throwing on an empty stream) by substituting `{}` so DELETE/PATCH
 * routes whose schemas have all-optional fields don't crash with a
 * 500 - the schema's own optionality decides whether the empty object
 * is a valid input.
 */
export async function parseBody<T extends ZodSchema>(
  req: NextRequest,
  schema: T,
): Promise<z.infer<T>> {
  const body = await req.json().catch(() => ({}));
  return schema.parse(body ?? {});
}

/**
 * Best-effort client IP from forwarded headers. The trusted proxy is
 * nginx (which sets `x-forwarded-for` from `$proxy_add_x_forwarded_for`),
 * so the leftmost token is the original client. Falls back to a literal
 * `unknown` so the per-IP key still exists when running outside the
 * proxy (dev, tests).
 */
export function clientIp(req: NextRequest): string {
  const xff = req.headers.get('x-forwarded-for');
  if (xff) {
    const first = xff.split(',')[0]?.trim();
    if (first) return first;
  }
  return req.headers.get('x-real-ip') ?? 'unknown';
}

/**
 * Wraps an unauthenticated route handler with a per-IP (or per-key) rate
 * limit. Used for portal/auth endpoints that have no session yet - the
 * `withRateLimit` helper in api/helpers.ts is keyed on `ctx.userId` and
 * cannot apply here.
 *
 * If `keySuffix` is provided, it's appended to the IP so a single client
 * IP can't exhaust an unrelated user's bucket (e.g. for sign-in we key
 * on `${ip}:${email}` so per-account brute force is the bottleneck and
 * a noisy NAT IP doesn't deny everyone).
 */
export async function enforcePublicRateLimit(
  req: NextRequest,
  name: RateLimiterName,
  keySuffix?: string,
): Promise<NextResponse | null> {
  const config = rateLimiters[name];
  const identifier = keySuffix ? `${clientIp(req)}:${keySuffix}` : clientIp(req);
  const result = await checkRateLimit(identifier, config);
  if (result.allowed) return null;
  const retryAfterSec = Math.max(1, Math.ceil((result.resetAt - Date.now()) / 1000));
  return NextResponse.json(
    { error: 'Too many requests. Please try again shortly.', retryAfter: retryAfterSec },
    {
      status: 429,
      headers: {
        ...rateLimitHeaders(result),
        'Retry-After': retryAfterSec.toString(),
      },
    },
  );
}
