/**
 * CORS hook utilities for @beignet/core/server
 */

import { BEIGNET_ERROR_OWNER_HEADER } from "../../contracts/types.js";
import type {
  HttpRequestLike,
  HttpResponseHeaders,
  ServerHook,
} from "../types.js";

/**
 * CORS configuration for `createCorsHooks(...)`.
 */
export interface CorsConfig {
  /**
   * Allowed origins. Use `"*"` only for non-credentialed requests.
   * Credentialed CORS requires an explicit origin allow-list.
   */
  origins?: string[] | "*";
  /**
   * Allowed HTTP methods.
   */
  methods?: string[];
  /**
   * Allowed request headers.
   */
  headers?: string[];
  /**
   * Additional response headers browser JavaScript may read. Beignet always
   * exposes its framework error-ownership header.
   */
  exposedHeaders?: string[];
  /**
   * Whether credentialed requests are allowed.
   */
  credentials?: boolean;
}

const DEFAULT_CORS: Required<CorsConfig> = {
  origins: "*",
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
  headers: ["Content-Type", "Authorization"],
  exposedHeaders: [],
  credentials: false,
};

function resolveCorsConfig(config: CorsConfig): Required<CorsConfig> {
  const resolved = {
    origins: config.origins ?? DEFAULT_CORS.origins,
    methods: config.methods ?? DEFAULT_CORS.methods,
    headers: config.headers ?? DEFAULT_CORS.headers,
    exposedHeaders: config.exposedHeaders ?? DEFAULT_CORS.exposedHeaders,
    credentials: config.credentials ?? DEFAULT_CORS.credentials,
  };

  if (
    resolved.credentials &&
    (resolved.origins === "*" ||
      (Array.isArray(resolved.origins) && resolved.origins.includes("*")))
  ) {
    throw new Error(
      'Credentialed CORS cannot use a wildcard origin. Use origins: ["https://app.example.com"] with credentials: true.',
    );
  }

  return resolved;
}

function appendVaryOrigin(headers: HttpResponseHeaders): void {
  const varyKey =
    Object.keys(headers).find((key) => key.toLowerCase() === "vary") ?? "Vary";
  const currentValue = headers[varyKey];
  const current =
    typeof currentValue === "string" ? currentValue : currentValue?.join(", ");
  const values = current?.split(",").map((value) => value.trim().toLowerCase());
  if (values?.includes("origin")) return;

  headers[varyKey] = current ? `${current}, Origin` : "Origin";
}

function appendCommaSeparatedHeader(
  headers: HttpResponseHeaders,
  name: string,
  appended: readonly string[],
): void {
  const key =
    Object.keys(headers).find(
      (candidate) => candidate.toLowerCase() === name.toLowerCase(),
    ) ?? name;
  const existing = headers[key];
  const values = [
    ...(typeof existing === "string"
      ? existing.split(",")
      : (existing ?? []).flatMap((value) => value.split(","))),
    ...appended,
  ]
    .map((value) => value.trim())
    .filter(Boolean);
  const seen = new Set<string>();
  const unique = values.filter((value) => {
    const normalized = value.toLowerCase();
    if (seen.has(normalized)) return false;
    seen.add(normalized);
    return true;
  });

  headers[key] = unique.join(", ");
}

/**
 * Apply CORS response headers to a mutable header record.
 *
 * Credentialed CORS rejects wildcard origins. Use an explicit origin allow-list
 * when cookies or authorization headers are allowed cross-origin.
 */
export function applyCorsHeaders(
  headers: HttpResponseHeaders,
  req: HttpRequestLike,
  corsConfig: CorsConfig,
): void {
  const {
    origins,
    methods,
    headers: allowedHeaders,
    exposedHeaders,
    credentials,
  } = resolveCorsConfig(corsConfig);

  if (origins === "*") {
    headers["Access-Control-Allow-Origin"] = "*";
  } else if (Array.isArray(origins)) {
    const requestOrigin = req.headers.get("Origin");
    if (requestOrigin && origins.includes(requestOrigin)) {
      headers["Access-Control-Allow-Origin"] = requestOrigin;
      appendVaryOrigin(headers);
    }
  }

  headers["Access-Control-Allow-Methods"] = methods.join(", ");
  headers["Access-Control-Allow-Headers"] = allowedHeaders.join(", ");
  appendCommaSeparatedHeader(headers, "Access-Control-Expose-Headers", [
    BEIGNET_ERROR_OWNER_HEADER,
    ...exposedHeaders,
  ]);

  if (credentials) {
    headers["Access-Control-Allow-Credentials"] = "true";
  }
}

/**
 * Create CORS hooks for preflight and regular responses.
 *
 * CORS preflight requests short-circuit with a 204 response. Explicit
 * `OPTIONS` routes without `Access-Control-Request-Method` continue through the
 * normal route pipeline. All responses are decorated in `beforeSend`.
 */
export function createCorsHooks<Ctx>(config: CorsConfig): ServerHook<Ctx> {
  const corsConfig = resolveCorsConfig(config);

  return {
    name: "cors",
    onRequest: ({ req }) => {
      if (
        req.method !== "OPTIONS" ||
        !req.headers.get("Origin") ||
        !req.headers.get("Access-Control-Request-Method")
      ) {
        return undefined;
      }
      const headers: HttpResponseHeaders = {};
      applyCorsHeaders(headers, req, corsConfig);
      return {
        status: 204,
        headers,
        body: null,
      };
    },
    beforeSend: ({ req, response }) => {
      const headers = { ...(response.headers ?? {}) };
      applyCorsHeaders(headers, req, corsConfig);
      return {
        ...response,
        headers,
      };
    },
  };
}
