import type { HttpRequestLike } from "./http.js";
import {
  parseHttpRequestUrl,
  requireTrustedProxyHeaderName,
} from "./trusted-proxy-internal.js";

/**
 * Header source used to resolve a client IP after an app has explicitly opted
 * into trusting its deployment proxy or edge.
 */
export type TrustedProxyClientIpSource =
  | "x-forwarded-for-last"
  | "x-forwarded-for-first"
  | "x-real-ip"
  | "cf-connecting-ip"
  | { header: string }
  | ((req: HttpRequestLike) => string | undefined);

/**
 * Trusted proxy configuration for request metadata.
 *
 * Beignet trusts no forwarding headers by default. Configure this only when
 * the app is always behind a platform or reverse proxy that strips or
 * normalizes these headers before they reach application code.
 */
export interface TrustedProxyOptions {
  /**
   * Header source for the end-user client IP. Omit or set to `false` when no
   * client-IP header should be trusted.
   */
  clientIp?: TrustedProxyClientIpSource | false;
  /**
   * Header that carries the external request protocol.
   *
   * Defaults to `x-forwarded-proto` when trusted proxy handling is enabled.
   */
  protocolHeader?: string | false;
  /**
   * Header that carries the external request host.
   *
   * Defaults to `x-forwarded-host` when trusted proxy handling is enabled.
   */
  hostHeader?: string | false;
}

/**
 * Set to `false` or omit the config to trust no forwarding headers.
 */
export type TrustedProxyConfig = false | TrustedProxyOptions;

/**
 * Request metadata after applying an explicit trusted-proxy policy.
 */
export interface TrustedRequestInfo {
  /**
   * URL as seen by the app or reconstructed from trusted proxy headers.
   */
  readonly url: Readonly<URL>;
  /**
   * External request origin.
   */
  readonly origin: string;
  /**
   * External request protocol without a trailing colon.
   */
  readonly protocol: "http" | "https";
  /**
   * External request host, including port when present.
   */
  readonly host: string;
  /**
   * Resolved client IP when a trusted client-IP source is configured.
   */
  readonly clientIp?: string;
  /**
   * Whether the server policy configured a trusted client-IP source. This can
   * be true while `clientIp` is absent when the expected header is missing.
   */
  readonly clientIpTrusted: boolean;
  /**
   * Whether forwarding headers were eligible to affect this result.
   */
  readonly trustedProxy: boolean;
}

const DEFAULT_PROTOCOL_HEADER = "x-forwarded-proto";
const DEFAULT_HOST_HEADER = "x-forwarded-host";

function splitForwardedHeader(value: string | null): string[] {
  if (!value) return [];
  return value
    .split(",")
    .map((entry) => entry.trim())
    .filter(Boolean);
}

function firstHeaderValue(
  req: HttpRequestLike,
  header: string,
): string | undefined {
  return splitForwardedHeader(req.headers.get(header))[0];
}

function normalizeProtocol(
  value: string | undefined,
): "http" | "https" | undefined {
  if (!value) return undefined;
  const protocol = value.toLowerCase().replace(/:$/, "");
  if (protocol === "http" || protocol === "https") return protocol;
  return undefined;
}

function hasInvalidHostCharacter(value: string): boolean {
  for (const char of value) {
    const code = char.charCodeAt(0);
    if (
      code <= 32 ||
      code === 127 ||
      char === "/" ||
      char === "\\" ||
      char === "@" ||
      char === "?" ||
      char === "#"
    ) {
      return true;
    }
  }
  return false;
}

function normalizeHost(
  value: string | undefined,
  protocol: "http" | "https",
): string | undefined {
  if (!value || hasInvalidHostCharacter(value)) {
    return undefined;
  }

  try {
    return new URL(`${protocol}://${value}`).host;
  } catch {
    return undefined;
  }
}

function baseRequestUrl(req: HttpRequestLike): URL {
  return parseHttpRequestUrl(req.url);
}

function normalizedBaseProtocol(url: URL): "http" | "https" {
  const protocol = normalizeProtocol(url.protocol);
  if (protocol) return protocol;
  throw new Error("Resolved request URL must use HTTP or HTTPS.");
}

/**
 * Resolve a client IP from a configured trusted proxy source.
 */
export function resolveTrustedClientIp(
  req: HttpRequestLike,
  source: TrustedProxyClientIpSource | false | undefined,
): string | undefined {
  if (!source) return undefined;

  if (typeof source === "function") {
    return source(req)?.trim() || undefined;
  }

  if (typeof source === "object") {
    return firstHeaderValue(
      req,
      requireTrustedProxyHeaderName(
        source.header,
        "trustedProxy.clientIp.header",
      ),
    );
  }

  if (source === "x-forwarded-for-first" || source === "x-forwarded-for-last") {
    const entries = splitForwardedHeader(req.headers.get("x-forwarded-for"));
    if (entries.length === 0) return undefined;
    return source === "x-forwarded-for-first"
      ? entries[0]
      : entries[entries.length - 1];
  }

  return firstHeaderValue(req, source);
}

/**
 * Resolve request metadata using only app-visible URL data unless a trusted
 * proxy policy is explicitly configured.
 */
export function resolveTrustedRequest(
  req: HttpRequestLike,
  config: TrustedProxyConfig | undefined = false,
): TrustedRequestInfo {
  const baseUrl = baseRequestUrl(req);
  const baseProtocol = normalizedBaseProtocol(baseUrl);
  const baseHost = baseUrl.host;

  if (!config) {
    return {
      url: baseUrl,
      origin: baseUrl.origin,
      protocol: baseProtocol,
      host: baseHost,
      clientIpTrusted: false,
      trustedProxy: false,
    };
  }

  const protocolHeader =
    config.protocolHeader === false
      ? undefined
      : requireTrustedProxyHeaderName(
          config.protocolHeader ?? DEFAULT_PROTOCOL_HEADER,
          "trustedProxy.protocolHeader",
        );
  const hostHeader =
    config.hostHeader === false
      ? undefined
      : requireTrustedProxyHeaderName(
          config.hostHeader ?? DEFAULT_HOST_HEADER,
          "trustedProxy.hostHeader",
        );

  const protocol =
    normalizeProtocol(
      protocolHeader ? firstHeaderValue(req, protocolHeader) : undefined,
    ) ?? baseProtocol;
  const host =
    normalizeHost(
      hostHeader ? firstHeaderValue(req, hostHeader) : undefined,
      protocol,
    ) ?? baseHost;
  const url = new URL(baseUrl);
  url.protocol = `${protocol}:`;
  url.host = host;
  const clientIp = resolveTrustedClientIp(req, config.clientIp);

  return {
    url,
    origin: url.origin,
    protocol,
    host,
    ...(clientIp !== undefined ? { clientIp } : {}),
    clientIpTrusted: Boolean(config.clientIp),
    trustedProxy: true,
  };
}
