import {
  getContractHeaderSchemas,
  type HttpContractConfig,
  methodSupportsRequestBody,
  type StandardSchema,
} from "../contracts/index.js";
import { decodeQueryValue } from "../query-codec.js";
import type { HttpRequestLike, HttpResponseLike } from "./http.js";
import {
  parseStandardSchema,
  SchemaValidationError,
} from "./providers/index.js";
import { errorResponse } from "./response-finalization.js";

/**
 * Request body limits enforced before contract body validation.
 */
export interface RequestBodyOptions {
  /**
   * Maximum request body size in bytes for JSON/text route bodies.
   *
   * @default 1048576
   */
  maxBytes?: number;
}

export type PreparedRequestInputs = {
  path: unknown;
  query: unknown;
  headers: unknown;
  body: unknown;
  rawHeaders: Record<string, string>;
};

export type RequestPreparationResult =
  | { ok: true; inputs: PreparedRequestInputs }
  | { ok: false; response: HttpResponseLike };

type RequestValidationLocation = "query" | "path" | "headers" | "body";

const DEFAULT_REQUEST_BODY_MAX_BYTES = 1024 * 1024;

class RequestBodyTooLargeError extends Error {
  readonly maxBytes: number;
  readonly actualBytes?: number;

  constructor(maxBytes: number, actualBytes?: number) {
    super("Request body exceeds the configured size limit.");
    this.name = "RequestBodyTooLargeError";
    this.maxBytes = maxBytes;
    this.actualBytes = actualBytes;
  }
}

class MalformedJsonBodyError extends Error {
  readonly parseError: unknown;

  constructor(parseError: unknown) {
    super("Request body contains malformed JSON.");
    this.name = "MalformedJsonBodyError";
    this.parseError = parseError;
  }
}

function contractDiagnostics(contract: HttpContractConfig) {
  return {
    contract: contract.name,
    method: contract.method,
    path: contract.path,
  };
}

function requestValidationDetails(
  contract: HttpContractConfig,
  location: RequestValidationLocation,
  error?: unknown,
  additionalDetails?: Record<string, unknown>,
) {
  const details = {
    ...contractDiagnostics(contract),
    location,
    ...additionalDetails,
  };

  if (error instanceof SchemaValidationError) {
    return {
      ...details,
      issues: error.issues,
    };
  }

  if (error instanceof Error) {
    return {
      ...details,
      message: error.message,
    };
  }

  return details;
}

function requestValidationError(
  contract: HttpContractConfig,
  status: number,
  code: string,
  message: string,
  location: RequestValidationLocation,
  error?: unknown,
  additionalDetails?: Record<string, unknown>,
): HttpResponseLike {
  return errorResponse(
    status,
    code,
    message,
    requestValidationDetails(contract, location, error, additionalDetails),
  );
}

function missingJsonContentTypeHint(
  req: HttpRequestLike,
  body: unknown,
): Record<string, unknown> | undefined {
  if (req.headers.get("content-type") || typeof body !== "string") {
    return undefined;
  }

  const trimmed = body.trim();
  if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) {
    return undefined;
  }

  try {
    const parsed = JSON.parse(trimmed);
    if (typeof parsed !== "object" || parsed === null) return undefined;
  } catch {
    return undefined;
  }

  return {
    hint: 'The request body looks like JSON. Set "Content-Type: application/json" to parse it as JSON.',
  };
}

export function requestHeadersToRecord(
  headers: Headers,
): Record<string, string> {
  const record: Record<string, string> = {};
  headers.forEach((value, key) => {
    record[key.toLowerCase()] = value;
  });
  return record;
}

async function parseHeaderSchemas(
  schemas: readonly StandardSchema[],
  rawHeaders: Record<string, string>,
): Promise<Record<string, unknown>> {
  let parsedHeaders: Record<string, unknown> = rawHeaders;

  for (const schema of schemas) {
    const parsed = await parseStandardSchema(schema, rawHeaders);
    if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
      parsedHeaders = {
        ...parsedHeaders,
        ...(parsed as Record<string, unknown>),
      };
    } else {
      parsedHeaders = parsed as Record<string, unknown>;
    }
  }

  return parsedHeaders;
}

export function requestBodyLimit(
  options: RequestBodyOptions | undefined,
): number {
  const maxBytes = options?.maxBytes ?? DEFAULT_REQUEST_BODY_MAX_BYTES;

  if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
    throw new Error(
      "createServer requestBody.maxBytes must be a positive number.",
    );
  }

  return maxBytes;
}

function assertContentLengthWithinLimit(
  headers: Headers,
  maxBytes: number,
): void {
  const contentLength = headers.get("content-length");
  if (contentLength === null) return;

  const actualBytes = Number(contentLength);
  if (!Number.isFinite(actualBytes) || actualBytes < 0) return;
  if (actualBytes > maxBytes) {
    throw new RequestBodyTooLargeError(maxBytes, actualBytes);
  }
}

async function readLimitedRequestText(
  req: HttpRequestLike,
  maxBytes: number,
): Promise<string> {
  assertContentLengthWithinLimit(req.headers, maxBytes);

  const body = req.raw?.body;
  if (!body) {
    const text = await req.text();
    const actualBytes = new TextEncoder().encode(text).byteLength;
    if (actualBytes > maxBytes) {
      throw new RequestBodyTooLargeError(maxBytes, actualBytes);
    }
    return text;
  }

  const reader = body.getReader();
  const decoder = new TextDecoder();
  let received = 0;
  let text = "";

  try {
    while (true) {
      const result = await reader.read();
      if (result.done) break;
      received += result.value.byteLength;
      if (received > maxBytes) {
        throw new RequestBodyTooLargeError(maxBytes, received);
      }
      text += decoder.decode(result.value, { stream: true });
    }
    text += decoder.decode();
  } finally {
    reader.releaseLock();
  }

  return text;
}

async function parseBody(
  req: HttpRequestLike,
  maxBytes: number,
): Promise<unknown> {
  const method = req.method.toUpperCase() as HttpContractConfig["method"];
  if (!methodSupportsRequestBody(method)) {
    return undefined;
  }

  const bodyReq = req.clone?.() ?? req;
  const contentType = req.headers.get("content-type") || "";
  if (contentType.includes("application/json")) {
    const text = await readLimitedRequestText(bodyReq, maxBytes);
    if (text === "") return undefined;
    try {
      return JSON.parse(text);
    } catch (error) {
      throw new MalformedJsonBodyError(error);
    }
  }

  const text = await readLimitedRequestText(bodyReq, maxBytes);
  return text === "" ? undefined : text;
}

export async function prepareRequestInputs(args: {
  contract: HttpContractConfig;
  req: HttpRequestLike;
  url: URL;
  rawHeaders: Record<string, string>;
  matchedParams: Record<string, string>;
  maxRequestBodyBytes: number;
  rawRoute?: boolean;
}): Promise<RequestPreparationResult> {
  const {
    contract,
    req,
    url,
    rawHeaders,
    matchedParams,
    maxRequestBodyBytes,
    rawRoute,
  } = args;
  const rawQuery: Record<string, unknown> = {};
  for (const key of new Set(url.searchParams.keys())) {
    const values = url.searchParams.getAll(key);
    const decoded = values.map(decodeQueryValue);
    rawQuery[key] = decoded.length === 1 ? decoded[0] : decoded;
  }

  let query: unknown = rawQuery;
  if (contract.query) {
    try {
      query = await parseStandardSchema(contract.query, query);
    } catch (error) {
      return {
        ok: false,
        response: requestValidationError(
          contract,
          422,
          "VALIDATION_ERROR",
          "Invalid query parameters",
          "query",
          error,
        ),
      };
    }
  }

  let path: unknown = matchedParams;
  if (contract.pathParams) {
    try {
      path = await parseStandardSchema(contract.pathParams, matchedParams);
    } catch (error) {
      return {
        ok: false,
        response: requestValidationError(
          contract,
          422,
          "VALIDATION_ERROR",
          "Invalid path parameters",
          "path",
          error,
        ),
      };
    }
  }

  let headers: unknown = rawHeaders;
  const headerSchemas = getContractHeaderSchemas(contract.headers);
  if (headerSchemas.length > 0) {
    try {
      headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
    } catch (error) {
      return {
        ok: false,
        response: requestValidationError(
          contract,
          422,
          "VALIDATION_ERROR",
          "Invalid request headers",
          "headers",
          error,
        ),
      };
    }
  }

  let body: unknown;
  // Raw routes own body consumption: the handler reads `req` itself, for
  // example to verify a webhook signature over the exact bytes.
  if (!rawRoute) {
    try {
      body = await parseBody(req, maxRequestBodyBytes);
    } catch (error) {
      if (error instanceof RequestBodyTooLargeError) {
        return {
          ok: false,
          response: requestValidationError(
            contract,
            413,
            "PAYLOAD_TOO_LARGE",
            "Request body is too large",
            "body",
            error,
          ),
        };
      }
      return {
        ok: false,
        response: requestValidationError(
          contract,
          400,
          "INVALID_BODY",
          error instanceof MalformedJsonBodyError
            ? "Malformed JSON"
            : "Could not read request body",
          "body",
          error instanceof MalformedJsonBodyError ? error.parseError : error,
        ),
      };
    }
  }

  if (contract.body) {
    try {
      body = await parseStandardSchema(contract.body, body);
    } catch (error) {
      if (body === undefined && error instanceof SchemaValidationError) {
        return {
          ok: false,
          response: requestValidationError(
            contract,
            400,
            "MISSING_BODY",
            "Request body is required",
            "body",
            error,
          ),
        };
      }
      return {
        ok: false,
        response: requestValidationError(
          contract,
          422,
          "VALIDATION_ERROR",
          "Invalid request body",
          "body",
          error,
          missingJsonContentTypeHint(req, body),
        ),
      };
    }
  }

  return {
    ok: true,
    inputs: { path, query, headers, body, rawHeaders },
  };
}
