// TOTP (RFC 6238) implemented on node:crypto — no runtime dependency.
//
// Why hand-rolled: the algorithm is small and stable, and avoiding a dep keeps
// the auth core auditable. Verified against the RFC 6238 Appendix B test vectors
// in test/auth/totp.test.js.
//
// Defaults match every mainstream authenticator app (Google Authenticator,
// Authy, 1Password): SHA-1, 6 digits, 30-second step.

import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';

const DIGITS = 6;
const STEP_SECONDS = 30;
const RFC4648_B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

// ── base32 (RFC 4648, no padding) ──────────────────────────────────────────
export function base32Encode(buf) {
  let bits = 0, value = 0, out = '';
  for (const byte of buf) {
    value = (value << 8) | byte;
    bits += 8;
    while (bits >= 5) {
      out += RFC4648_B32[(value >>> (bits - 5)) & 31];
      bits -= 5;
    }
  }
  if (bits > 0) out += RFC4648_B32[(value << (5 - bits)) & 31];
  return out;
}

export function base32Decode(str) {
  const clean = str.replace(/=+$/,'').toUpperCase().replace(/\s+/g, '');
  let bits = 0, value = 0;
  const out = [];
  for (const ch of clean) {
    const idx = RFC4648_B32.indexOf(ch);
    if (idx === -1) continue; // skip stray chars
    value = (value << 5) | idx;
    bits += 5;
    if (bits >= 8) {
      out.push((value >>> (bits - 8)) & 0xff);
      bits -= 8;
    }
  }
  return Buffer.from(out);
}

// Generate a new base32 secret (20 random bytes = 160 bits, the RFC-recommended
// SHA-1 key length).
export function generateSecret() {
  return base32Encode(randomBytes(20));
}

// HOTP for a specific counter (RFC 4226).
function hotp(secretBuf, counter) {
  const buf = Buffer.alloc(8);
  // 64-bit big-endian counter.
  buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);
  buf.writeUInt32BE(counter >>> 0, 4);
  const hmac = createHmac('sha1', secretBuf).update(buf).digest();
  const offset = hmac[hmac.length - 1] & 0x0f;
  const code = ((hmac[offset] & 0x7f) << 24)
             | ((hmac[offset + 1] & 0xff) << 16)
             | ((hmac[offset + 2] & 0xff) << 8)
             | (hmac[offset + 3] & 0xff);
  return String(code % (10 ** DIGITS)).padStart(DIGITS, '0');
}

// The TOTP code for a given time (defaults to now).
export function generateToken(base32Secret, atMs = Date.now()) {
  const counter = Math.floor(atMs / 1000 / STEP_SECONDS);
  return hotp(base32Decode(base32Secret), counter);
}

// Verify a user-supplied code, allowing ±`window` steps of clock drift
// (default ±1 = 90s total tolerance). Constant-time compare per candidate.
//
// Returns the matched counter on success (so callers can persist it for
// replay protection — RFC 6238 §5.2), or null on failure. Boolean truthiness
// still works for the common case (`if (verifyToken(...))`).
export function verifyToken(base32Secret, token, atMs = Date.now(), window = 1) {
  if (!base32Secret || !token) return null;
  const cleaned = String(token).replace(/\s+/g, '');
  if (!/^\d{6}$/.test(cleaned)) return null;
  const secretBuf = base32Decode(base32Secret);
  const counter = Math.floor(atMs / 1000 / STEP_SECONDS);
  const want = Buffer.from(cleaned);
  for (let w = -window; w <= window; w++) {
    const candidate = Buffer.from(hotp(secretBuf, counter + w));
    if (candidate.length === want.length && timingSafeEqual(candidate, want)) return counter + w;
  }
  return null;
}

// The otpauth:// URI an authenticator app scans. label/issuer show in the app.
export function otpauthURI(base32Secret, accountName, issuer = 'Dragonflight') {
  const label = encodeURIComponent(`${issuer}:${accountName}`);
  const params = new URLSearchParams({
    secret: base32Secret,
    issuer,
    algorithm: 'SHA1',
    digits: String(DIGITS),
    period: String(STEP_SECONDS),
  });
  return `otpauth://totp/${label}?${params.toString()}`;
}

// Generate N human-friendly one-time recovery codes (raw form). Caller hashes
// them before storage and shows the raw set to the user exactly once.
export function generateRecoveryCodes(n = 10) {
  const codes = [];
  for (let i = 0; i < n; i++) {
    // 10 hex chars in two dash-separated groups, e.g. "a1b2c-3d4e5".
    const hex = randomBytes(5).toString('hex');
    codes.push(hex.slice(0, 5) + '-' + hex.slice(5));
  }
  return codes;
}
