/**
 * @wrnexus/helpers — safe conveniences for common WrNexus application flows.
 *
 * Helpers stay small and composable. They accept the standard WrNexus Context
 * and return web-platform values such as URL and Response.
 */

import type { Context } from "@wrnexus/core";
import { currentAppOrigin } from "./workspace.ts";

export type RequestContext = Pick<Context, "req" | "url">;

export type AllowedHosts =
  readonly string[] | ReadonlySet<string> | ((host: string, ctx: RequestContext) => boolean);

export interface OriginalRequestOptions {
  /**
   * Hosts that the application permits as redirect destinations. This is
   * required when a proxy supplied X-Forwarded-Host is present.
   */
  allowedHosts?: AllowedHosts;
}

export interface LoginRedirectOptions extends OriginalRequestOptions {
  /** Query parameter that receives the original absolute URL. */
  returnToParam?: string;
  /** Browser redirect status. Defaults to 302. */
  status?: 301 | 302 | 303 | 307 | 308;
}

function forwardedValue(ctx: RequestContext, name: string): string | null {
  const value = ctx.req.headers.get(name)?.trim();
  if (!value) return null;
  if (value.includes(",")) {
    throw new TypeError(`${name} must contain exactly one value`);
  }
  return value;
}

function hostIsAllowed(host: string, ctx: RequestContext, allowedHosts?: AllowedHosts): boolean {
  if (!allowedHosts) return false;
  if (typeof allowedHosts === "function") return allowedHosts(host, ctx);

  const normalized = host.toLowerCase();
  for (const allowed of allowedHosts) {
    if (allowed.toLowerCase() === normalized) return true;
  }
  return false;
}

/** Get the original path and query string seen by the gateway. */
export function getOriginalRequestPath(ctx: RequestContext): string {
  const value = forwardedValue(ctx, "x-original-uri") ?? `${ctx.url.pathname}${ctx.url.search}`;
  if (
    !value.startsWith("/") ||
    value.startsWith("//") ||
    value.includes("\\") ||
    value.includes("#")
  ) {
    throw new TypeError("x-original-uri must be an absolute request path");
  }
  return value;
}

/** Get the original HTTP method seen by the gateway. */
export function getOriginalRequestMethod(ctx: RequestContext): string {
  const method = forwardedValue(ctx, "x-original-method") ?? ctx.req.method;
  if (!/^[A-Za-z]+$/.test(method)) throw new TypeError("x-original-method is invalid");
  return method.toUpperCase();
}

/**
 * Reconstruct the absolute URL that reached the gateway.
 *
 * Forwarded hosts are never trusted implicitly: pass allowedHosts when this is
 * used behind the WrNexus gateway. Direct requests fall back to ctx.url.
 */
export function getOriginalRequestUrl(
  ctx: RequestContext,
  options: OriginalRequestOptions = {},
): URL {
  // A forward-auth verifier can itself sit behind the same gateway. In that
  // nested hop X-Forwarded-Host correctly describes the verifier (SSO), while
  // X-Original-Host keeps the protected application's host (for returnTo).
  const host = forwardedValue(ctx, "x-original-host") ?? forwardedValue(ctx, "x-forwarded-host");
  const path = getOriginalRequestPath(ctx);

  if (!host) return new URL(path, ctx.url.origin);
  if (!hostIsAllowed(host, ctx, options.allowedHosts)) {
    throw new TypeError(`Untrusted forwarded host: ${host}`);
  }

  const protocol = (
    forwardedValue(ctx, "x-original-proto") ??
    forwardedValue(ctx, "x-forwarded-proto") ??
    ctx.url.protocol
  ).replace(/:$/, "");
  if (protocol !== "http" && protocol !== "https") {
    throw new TypeError(`Unsupported forwarded protocol: ${protocol}`);
  }

  const origin = new URL(`${protocol}://${host}`);
  if (origin.username || origin.password || origin.pathname !== "/") {
    throw new TypeError(`Invalid forwarded host: ${host}`);
  }

  const original = new URL(path, origin);
  if (original.origin !== origin.origin) {
    throw new TypeError("Original request URL must stay on the forwarded origin");
  }
  return original;
}

/** Get the original request origin, for example http://admin.localhost:3000. */
export function getOriginalRequestOrigin(
  ctx: RequestContext,
  options: OriginalRequestOptions = {},
): string {
  return getOriginalRequestUrl(ctx, options).origin;
}

/**
 * Redirect to a login page with the original absolute URL encoded as returnTo.
 * Relative login URLs resolve against the current app (normally the SSO app).
 */
export function redirectToLogin(
  ctx: RequestContext,
  loginUrl: string | URL,
  options: LoginRedirectOptions = {},
): Response {
  const target = new URL(loginUrl, `${currentAppOrigin() ?? ctx.url.origin}/`);
  if (target.protocol !== "http:" && target.protocol !== "https:") {
    throw new TypeError("Login URL must use http or https");
  }

  const original = getOriginalRequestUrl(ctx, options);
  target.searchParams.set(options.returnToParam ?? "returnTo", original.href);
  return Response.redirect(target, options.status ?? 302);
}

export {
  appOrigin,
  appUrl,
  currentAppName,
  currentAppOrigin,
  workspaceAppOrigins,
  workspaceRootDomain,
} from "./workspace.ts";
export {
  backoffDelay,
  sleep,
  retry,
  withTimeout,
  stableStringify,
  safeJsonParse,
  clamp,
  once,
} from "./resilience.ts";
export type { RetryOptions } from "./resilience.ts";
