export interface GraphqlRequest {
  query: string;
  variables?: Record<string, unknown>;
  operationName?: string;
}
export interface GraphqlExecutionResult {
  data?: unknown;
  errors?: Array<{ message: string; [key: string]: unknown }>;
}
export interface GraphqlOptions {
  execute(
    request: GraphqlRequest,
    context: { request: Request; signal: AbortSignal },
  ): GraphqlExecutionResult | Promise<GraphqlExecutionResult>;
  maxQueryBytes?: number;
  maxDepth?: number;
  maxAliases?: number;
  allowIntrospection?: boolean;
}

function queryMetrics(query: string) {
  let depth = 0,
    maxDepth = 0,
    aliases = 0;
  let string = false,
    escaped = false;
  for (let index = 0; index < query.length; index++) {
    const char = query[index]!;
    if (string) {
      if (escaped) escaped = false;
      else if (char === "\\") escaped = true;
      else if (char === '"') string = false;
      continue;
    }
    if (char === '"') string = true;
    else if (char === "{") maxDepth = Math.max(maxDepth, ++depth);
    else if (char === "}") depth--;
    else if (char === ":" && /[A-Za-z0-9_]\s*$/.test(query.slice(Math.max(0, index - 40), index)))
      aliases++;
  }
  return { depth: maxDepth, aliases };
}

export function createGraphqlHandler(options: GraphqlOptions) {
  return async (request: Request): Promise<Response> => {
    if (request.method !== "POST")
      return new Response("Method Not Allowed", { status: 405, headers: { allow: "POST" } });
    const maxBytes = options.maxQueryBytes ?? 100_000;
    const length = Number(request.headers.get("content-length") ?? 0);
    if (length > maxBytes)
      return Response.json(
        { errors: [{ message: "GraphQL query is too large" }] },
        { status: 413 },
      );
    const body = (await request.json().catch(() => null)) as GraphqlRequest | null;
    if (
      !body ||
      typeof body.query !== "string" ||
      new TextEncoder().encode(body.query).length > maxBytes
    )
      return Response.json(
        { errors: [{ message: "A bounded GraphQL query string is required" }] },
        { status: 400 },
      );
    if (options.allowIntrospection === false && /\b__(?:schema|type)\b/.test(body.query))
      return Response.json(
        { errors: [{ message: "GraphQL introspection is disabled" }] },
        { status: 403 },
      );
    const metrics = queryMetrics(body.query);
    if (metrics.depth > (options.maxDepth ?? 12) || metrics.aliases > (options.maxAliases ?? 50))
      return Response.json(
        { errors: [{ message: "GraphQL query complexity limit exceeded" }] },
        { status: 400 },
      );
    try {
      const result = await options.execute(body, { request, signal: request.signal });
      return Response.json(result, {
        status: result.errors?.length && result.data === undefined ? 400 : 200,
        headers: { "cache-control": "no-store" },
      });
    } catch {
      return Response.json({ errors: [{ message: "GraphQL execution failed" }] }, { status: 500 });
    }
  };
}

export { graphqlPlugin } from "./plugin.ts";
