import type { Context, Middleware, RequestLimitsConfig } from "@wrnexus/core";

export type RequestHardeningOptions = RequestLimitsConfig;

const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);

function headerBytes(headers: Headers): { count: number; bytes: number } {
  let count = 0;
  let bytes = 0;
  headers.forEach((value, name) => {
    count++;
    bytes += new TextEncoder().encode(`${name}:${value}\r\n`).byteLength;
  });
  return { count, bytes };
}

function hostAllowed(host: string, rules: string[]): boolean {
  const normalized = host.toLowerCase().split(":")[0]!;
  return rules.some((rule) => {
    const candidate = rule.toLowerCase();
    return candidate.startsWith("*.")
      ? normalized.endsWith(candidate.slice(1))
      : normalized === candidate;
  });
}

function reject(status: number, message: string): Response {
  return new Response(message, {
    status,
    headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" },
  });
}

export function requestHardening(options: RequestHardeningOptions = {}): Middleware {
  const maxUrlLength = options.maxUrlLength ?? 8_192;
  const maxHeaderCount = options.maxHeaderCount ?? 100;
  const maxHeaderBytes = options.maxHeaderBytes ?? 32 * 1024;
  const maxQueryParameters = options.maxQueryParameters ?? 200;
  const maxBodyBytes = options.maxBodyBytes ?? 10 * 1024 * 1024;
  const timeoutMs = options.timeoutMs ?? 30_000;
  const maxConcurrent = options.maxConcurrent ?? 1_000;
  let concurrent = 0;

  return async (ctx: Context, next) => {
    if (ctx.req.url.length > maxUrlLength) return reject(414, "URI Too Long");
    const measured = headerBytes(ctx.req.headers);
    if (measured.count > maxHeaderCount || measured.bytes > maxHeaderBytes) {
      return reject(431, "Request Header Fields Too Large");
    }
    if ([...ctx.url.searchParams].length > maxQueryParameters) {
      return reject(400, "Too many query parameters");
    }
    const contentLength = Number(ctx.req.headers.get("content-length") ?? "0");
    if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
      return reject(413, "Payload Too Large");
    }
    if (options.trustedHosts?.length) {
      const host = ctx.req.headers.get("host") ?? ctx.url.host;
      if (!hostAllowed(host, options.trustedHosts)) return reject(421, "Misdirected Request");
    }
    if (options.fetchMetadata !== false && !SAFE_METHODS.has(ctx.req.method.toUpperCase())) {
      const site = ctx.req.headers.get("sec-fetch-site");
      if (site === "cross-site") return reject(403, "Cross-site request denied");
    }
    if (concurrent >= maxConcurrent) return reject(503, "Server Busy");

    concurrent++;
    let timer: ReturnType<typeof setTimeout> | undefined;
    try {
      const timeout = new Promise<Response>((resolve) => {
        timer = setTimeout(() => resolve(reject(504, "Request Timeout")), timeoutMs);
      });
      return await Promise.race([Promise.resolve(next()), timeout]);
    } finally {
      if (timer) clearTimeout(timer);
      concurrent--;
    }
  };
}
