/**
 * Public App URLs
 *
 * Where the app lives on the internet. Payment gateways redirect the buyer back
 * to these URLs from their own hosted checkout pages, so they must be publicly
 * reachable — a `localhost` value strands the customer after they have paid.
 *
 * Kept in its own module (no model or service imports) so it can be pulled into
 * `index.ts` and the payment service without dragging anything else along.
 */

/** Production app domain — the default target for every gateway redirect. */
export const PRODUCTION_APP_URL = 'https://app.mengoengine.com';

/** True for any address a gateway could not redirect a real customer to. */
function isLocalAddress(url: string): boolean {
  return /localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]/i.test(url);
}

/**
 * Base URL for anything a person opens from OUTSIDE the app — password reset
 * links, invitation links, the footer links in every transactional email.
 *
 * Resolution order:
 *   1. `PUBLIC_APP_URL` — explicit override for a staging or white-label domain
 *   2. `FRONTEND_URL`, but only when it is publicly reachable
 *   3. the production domain
 *
 * The guard matters because `FRONTEND_URL` is also the CORS origin, and on a
 * single-host deployment that is legitimately `http://localhost:3100` — the
 * container talks to the frontend over the loopback interface. Correct for
 * CORS, useless in an email: the recipient's browser is on another machine.
 */
export function getPublicAppBaseUrl(): string {
  const explicit = process.env.PUBLIC_APP_URL?.trim();
  if (explicit) return explicit.replace(/\/+$/, '');

  const configured = process.env.FRONTEND_URL?.trim();
  if (configured && !isLocalAddress(configured)) {
    return configured.replace(/\/+$/, '');
  }

  if (configured) {
    console.warn(
      `[AppUrls] FRONTEND_URL "${configured}" is a local address — emailed links would be unopenable. Using ${PRODUCTION_APP_URL}. Set PUBLIC_APP_URL to override.`
    );
  }
  return PRODUCTION_APP_URL;
}

// ============================================
// Per-request base URL
// ============================================

/**
 * The part of an incoming request this module reads. Declared structurally so
 * appUrls stays free of an express import, per the note at the top of the file.
 */
export interface OriginBearingRequest {
  headers: Record<string, string | string[] | undefined>;
}

function isProduction(): boolean {
  return process.env.NODE_ENV === 'production';
}

function firstHeader(req: OriginBearingRequest, name: string): string | undefined {
  const raw = req.headers?.[name];
  return Array.isArray(raw) ? raw[0] : raw;
}

/**
 * The host the BROWSER asked for, as opposed to the host this process is
 * listening on.
 *
 * The frontend proxies `/api/*` to the backend (see next.config rewrites), so
 * `Host` here is always the internal target — `localhost:3101`. Next forwards
 * the real one as `x-forwarded-host`, and any reverse proxy in front of a
 * deployment does the same, so that is the header worth reading. A comma-joined
 * value means several proxies appended to it; the first entry is the original.
 */
function requestForwardedHost(req: OriginBearingRequest): string | null {
  const forwarded = firstHeader(req, 'x-forwarded-host')?.split(',')[0]?.trim();
  if (forwarded) return forwarded.toLowerCase();

  // No proxy in front: the browser reached this process directly, so `Host` is
  // the public one. Skipped when it is a local address, because that is the
  // internal rewrite target (`localhost:3101`) rather than anything a browser
  // typed — trusting it would make every proxied request look same-origin.
  const host = firstHeader(req, 'host')?.trim();
  if (host && !isLocalAddress(host)) return host.toLowerCase();

  return null;
}

/** Reduce any URL to `scheme://host[:port]`, or null if it is not a usable http(s) URL. */
function normalizeOrigin(value: string): string | null {
  try {
    const url = new URL(value.trim());
    if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
    return `${url.protocol}//${url.host}`.toLowerCase();
  } catch {
    return null;
  }
}

/**
 * Origins this deployment is willing to put in an email.
 *
 * `FRONTEND_URL` is included only when it is publicly reachable, or when we are
 * not in production — otherwise the loopback CORS origin would creep back into
 * live links, which is the bug this whole module exists to prevent.
 */
function allowedOrigins(): Set<string> {
  const origins = new Set<string>();
  const add = (value?: string | null): void => {
    const normalized = value ? normalizeOrigin(value) : null;
    if (normalized) origins.add(normalized);
  };

  add(PRODUCTION_APP_URL);
  add(process.env.PUBLIC_APP_URL);

  const configured = process.env.FRONTEND_URL?.trim();
  if (configured && (!isLocalAddress(configured) || !isProduction())) add(configured);

  // Extra hosts a deployment legitimately answers on (staging, white-label).
  for (const entry of (process.env.ALLOWED_APP_ORIGINS || '').split(',')) add(entry);

  return origins;
}

/**
 * Base URL for a link mailed in response to THIS request, so a reset requested
 * from localhost is openable on localhost and one requested from the live app
 * points at the live app.
 *
 * The origin is taken from the request but never trusted on its own: it is
 * matched against allowedOrigins() first. An unchecked origin here is the
 * classic password-reset poisoning route — request a reset for someone else's
 * address with a forged Origin/Host, and the token is mailed to the victim as a
 * link to the attacker's domain. Anything unrecognised falls back to the
 * configured public URL, so the worst case is a link to the real app.
 *
 * Local origins are additionally accepted outside production, which is what
 * makes `npm run dev` mail a working localhost link.
 *
 * A brand-new domain cannot be in any allowlist by definition, so there is one
 * more rule: an origin is accepted when it matches the host the browser
 * actually asked for (`x-forwarded-host`). That is the app talking to its own
 * backend, whatever domain it has been moved to, so no configuration is needed
 * after a move. A forged `Origin` alone fails it — the attacker's request still
 * carries the real deployment's forwarded host.
 *
 * That last rule assumes the backend is only reachable through the proxy that
 * sets `x-forwarded-host` (it listens on localhost and the frontend rewrites to
 * it). If the backend is ever exposed directly, a caller could set both headers
 * itself; set `STRICT_APP_ORIGIN=true` to switch the rule off and go back to
 * allowlist-only, or pin the domain with `PUBLIC_APP_URL`.
 */
export function getRequestAppBaseUrl(req?: OriginBearingRequest | null): string {
  const fallback = getPublicAppBaseUrl();
  if (!req?.headers) return fallback;

  const origin = firstHeader(req, 'origin');
  const referer = firstHeader(req, 'referer');
  const candidate = (origin ? normalizeOrigin(origin) : null) || (referer ? normalizeOrigin(referer) : null);
  if (!candidate) return fallback;

  if (allowedOrigins().has(candidate)) return candidate;
  if (!isProduction() && isLocalAddress(candidate)) return candidate;

  // Same-origin request: the browser asked for this host and the page it came
  // from is on that same host, so the link belongs there.
  if (process.env.STRICT_APP_ORIGIN !== 'true') {
    // `candidate` is already `scheme://host`, so the only `//` in it is the one
    // after the scheme — this compares hosts, not a substring anywhere.
    const forwardedHost = requestForwardedHost(req);
    if (forwardedHost && candidate.endsWith(`//${forwardedHost}`)) {
      return candidate;
    }
  }

  console.warn(
    `[AppUrls] Request origin "${candidate}" is not an allowed app origin — using ${fallback}. Add it to ALLOWED_APP_ORIGINS if it is legitimate.`
  );
  return fallback;
}

/**
 * Base URL for gateway success / cancel / verification redirects.
 *
 * Resolution order:
 *   1. `STRIPE_REDIRECT_BASE_URL` — explicit override for a staging domain or
 *      an ngrok tunnel when callbacks genuinely need to reach a non-prod host
 *   2. `FRONTEND_URL`, but only when it is publicly reachable
 *   3. the production domain
 *
 * A local `FRONTEND_URL` is deliberately ignored here: it is correct for CORS
 * during development but useless as a gateway callback target.
 */
export function getPaymentRedirectBaseUrl(): string {
  const explicit = process.env.STRIPE_REDIRECT_BASE_URL?.trim();
  if (explicit) return explicit.replace(/\/+$/, '');

  const configured = process.env.FRONTEND_URL?.trim();
  if (configured && !isLocalAddress(configured)) {
    return configured.replace(/\/+$/, '');
  }

  if (configured) {
    console.warn(
      `[AppUrls] FRONTEND_URL "${configured}" is a local address — payment gateways cannot redirect there. Using ${PRODUCTION_APP_URL}. Set STRIPE_REDIRECT_BASE_URL to override.`
    );
  }
  return PRODUCTION_APP_URL;
}
