import { createHash } from 'crypto';
import type { Request } from 'express';

/**
 * Device fingerprint binding for refresh tokens (added 2026-04-12).
 *
 * A refresh token stolen via XSS, log exfiltration, or database breach is
 * currently valid from any IP/device until natural expiry. To close that
 * window, we bind each refresh token to the issuing device by embedding a
 * `df` claim (SHA-256 of user-agent + /24-masked IP) in the refresh JWT, and
 * reject refresh attempts whose fingerprint doesn't match.
 *
 * We mask to /24 (IPv4) and /48 (IPv6) so legitimate mobile network changes
 * within the same carrier subnet don't force re-login on every tower hop,
 * while cross-country or VPN changes do.
 */

/**
 * Returns the /24-masked IPv4 or /48-masked IPv6 subnet portion of the
 * client address. Trusts `req.ip` which Express populates from X-Forwarded-For
 * when `trust proxy` is set.
 */
function maskIp(rawIp: string | undefined): string {
  if (!rawIp) return 'unknown';
  const ip = rawIp.replace(/^::ffff:/, '').trim();
  if (ip.includes(':')) {
    // IPv6: take first 3 hextets (/48)
    return ip.split(':').slice(0, 3).join(':');
  }
  const parts = ip.split('.');
  if (parts.length === 4) {
    return `${parts[0]}.${parts[1]}.${parts[2]}.0`;
  }
  return ip;
}

/**
 * Compute a stable fingerprint for the incoming request. Include major UA
 * (Chrome/Firefox/Safari etc.) but not full string — we want stability across
 * minor browser upgrades while still catching device swaps.
 */
export function computeDeviceFingerprint(req: Request): string {
  const ua = (req.headers['user-agent'] ?? '').toString().slice(0, 200);
  const ipSubnet = maskIp(req.ip);
  return createHash('sha256').update(`${ua}|${ipSubnet}`).digest('hex').slice(0, 32);
}

/**
 * Constant-time compare to avoid timing side channels when an attacker is
 * testing stolen tokens against different fingerprints.
 */
export function fingerprintsMatch(a: string | undefined, b: string | undefined): boolean {
  if (!a || !b || a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return diff === 0;
}
