import { NextResponse } from "next/server";

/**
 * True when the request advertises a `Content-Length` larger than `maxBytes`.
 *
 * Malformed or negative values are deliberately NOT treated as exceeded: they
 * cannot be trusted as an upper bound, so the body is still read and measured
 * afterwards (see `readJsonSized`). A missing header is the chunked-encoding
 * case and falls through the same post-parse measurement.
 */
export function contentLengthExceeded(request: Request, maxBytes: number): boolean {
  const raw = request.headers.get("content-length");
  if (raw === null) return false;

  const length = Number(raw);
  if (!Number.isFinite(length) || length < 0) return false;

  return length > maxBytes;
}

/**
 * Rejects a request before its body is buffered when the advertised
 * `Content-Length` already exceeds the limit. Returns null when the request
 * may proceed. This is the guard a 2-GB upload must hit BEFORE `formData()` /
 * `json()` / `text()` ever start reading the stream.
 */
export function guardBodySize(request: Request, maxBytes: number): NextResponse | null {
  if (!contentLengthExceeded(request, maxBytes)) return null;
  return NextResponse.json({ error: "request_too_large", maxBytes }, { status: 413 });
}

/**
 * Replacement for naked `req.json()` calls.
 *
 * 1. Rejects via `Content-Length` before parsing (covers well-behaved clients
 *    and keeps huge bodies from being buffered at all).
 * 2. Parses the body; a parse failure yields `400 { error: "invalid_json" }`.
 * 3. Measures the serialized size afterwards and rejects oversized payloads
 *    that slipped past step 1 (chunked bodies or missing headers).
 */
class PayloadTooLargeError extends Error {}

/**
 * Replacement for naked `req.formData()`.
 *
 * `guardBodySize` alone only rejects a spoofed/oversized `Content-Length`
 * header — a chunked request that omits the header (or lies about it) would
 * otherwise sail past that check and let `formData()` buffer an unbounded
 * body before `file.size` is ever inspected. This wraps the body stream and
 * aborts the read the moment the byte count crosses `maxBytes`, so the cap
 * holds even without a trustworthy `Content-Length`.
 */
export async function readFormDataSized(
  request: Request,
  maxBytes: number
): Promise<{ ok: true; formData: FormData } | { ok: false; response: NextResponse }> {
  const guard = guardBodySize(request, maxBytes);
  if (guard) return { ok: false, response: guard };

  const body = request.body;
  if (!body) {
    const formData = await request.formData();
    return { ok: true, formData };
  }

  let total = 0;
  const reader = body.getReader();
  const limited = new ReadableStream<Uint8Array>({
    async pull(controller) {
      const { done, value } = await reader.read();
      if (done) {
        controller.close();
        return;
      }
      total += value.byteLength;
      if (total > maxBytes) {
        controller.error(new PayloadTooLargeError());
        await reader.cancel();
        return;
      }
      controller.enqueue(value);
    },
    cancel(reason) {
      reader.cancel(reason);
    },
  });

  const bounded = new Request(request.url, {
    method: request.method,
    headers: request.headers,
    body: limited,
    duplex: "half",
  } as RequestInit & { duplex: "half" });

  try {
    const formData = await bounded.formData();
    return { ok: true, formData };
  } catch (err) {
    if (err instanceof PayloadTooLargeError) {
      return {
        ok: false,
        response: NextResponse.json({ error: "request_too_large", maxBytes }, { status: 413 }),
      };
    }
    throw err;
  }
}

export async function readJsonSized<T>(
  request: Request,
  maxBytes: number
): Promise<{ ok: true; body: T } | { ok: false; response: NextResponse }> {
  const guard = guardBodySize(request, maxBytes);
  if (guard) return { ok: false, response: guard };

  let parsed: unknown;
  try {
    parsed = await request.json();
  } catch {
    return {
      ok: false,
      response: NextResponse.json({ error: "invalid_json" }, { status: 400 }),
    };
  }

  // `JSON.stringify(undefined)` returns undefined — guard the type and the
  // measurement by falling back to an empty string.
  const serialized = JSON.stringify(parsed) ?? "";
  if (serialized.length > maxBytes) {
    return {
      ok: false,
      response: NextResponse.json(
        { error: "request_too_large", maxBytes },
        { status: 413 }
      ),
    };
  }

  return { ok: true, body: parsed as T };
}
