import { timingSafeEqual } from 'crypto';

/**
 * Authenticates an inbound Pluggy webhook. Pluggy does NOT sign its webhooks
 * (no HMAC, no signature header — confirmed at
 * https://docs.pluggy.ai/docs/webhooks). Authentication is therefore:
 *
 *   1. IP allowlist — Pluggy sends from a fixed source IP (177.71.238.212).
 *      If `allowedIps` is empty the IP check is SKIPPED (header-only mode).
 *   2. Secret custom header — set on the webhook at registration via Pluggy's
 *      API, verified here with a constant-time compare against the expected
 *      token. Fail-closed (false) on any empty/mismatched/length-differing input.
 *
 * Returns true only when BOTH checks pass. The token is never logged.
 */
export function isPluggyRequestAuthentic(params: {
  ip: string | undefined;
  allowedIps: string[];
  presentedToken: string | undefined;
  expectedToken: string;
}): boolean {
  const { ip, allowedIps, presentedToken, expectedToken } = params;

  if (!ipAllowed(ip, allowedIps)) return false;
  if (!tokenMatches(presentedToken, expectedToken)) return false;
  return true;
}

/** Empty allowlist ⇒ skip (header-only). Otherwise the IP must be listed. */
function ipAllowed(ip: string | undefined, allowedIps: string[]): boolean {
  if (!allowedIps || allowedIps.length === 0) return true;
  if (!ip) return false;
  const normalized = normalizeIp(ip);
  return allowedIps.some((allowed) => normalizeIp(allowed) === normalized);
}

/**
 * Normalize an IP for comparison. Node/Express surfaces dual-stack sockets as
 * IPv4-mapped IPv6 (e.g. `::ffff:177.71.238.212`); strip that prefix so a bare
 * IPv4 allowlist entry still matches. Case-insensitive for IPv6 hex.
 */
function normalizeIp(ip: string): string {
  const trimmed = ip.trim().toLowerCase();
  return trimmed.startsWith('::ffff:')
    ? trimmed.slice('::ffff:'.length)
    : trimmed;
}

/** Constant-time compare with a length guard (different lengths ⇒ false, no throw). */
function tokenMatches(
  presented: string | undefined,
  expected: string,
): boolean {
  if (!presented || presented.length === 0) return false;
  if (!expected || expected.length === 0) return false;

  const a = Buffer.from(presented);
  const b = Buffer.from(expected);
  if (a.length !== b.length) return false; // timingSafeEqual throws on length mismatch

  return timingSafeEqual(a, b);
}
