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

/**
 * TOTP (RFC 6238) utan externa beroenden – HMAC-SHA1, 6 siffror, 30 s steg.
 * Används för admin-2FA. Inga tredjepartspaket i autentiseringskedjan
 * (samma princip som scrypt-valet för lösenord).
 */

const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

export function base32Encode(buf: Buffer): string {
  let bits = 0;
  let value = 0;
  let out = "";
  for (const byte of buf) {
    value = (value << 8) | byte;
    bits += 8;
    while (bits >= 5) {
      out += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
      bits -= 5;
    }
  }
  if (bits > 0) out += BASE32_ALPHABET[(value << (5 - bits)) & 31];
  return out;
}

export function base32Decode(s: string): Buffer {
  const clean = s.toUpperCase().replace(/=+$/, "").replace(/\s/g, "");
  let bits = 0;
  let value = 0;
  const out: number[] = [];
  for (const ch of clean) {
    const idx = BASE32_ALPHABET.indexOf(ch);
    if (idx === -1) throw new Error("Ogiltig base32");
    value = (value << 5) | idx;
    bits += 5;
    if (bits >= 8) {
      out.push((value >>> (bits - 8)) & 0xff);
      bits -= 8;
    }
  }
  return Buffer.from(out);
}

/** Nytt slumpat TOTP-secret (160 bitar enligt RFC 4226-rekommendation). */
export function generateTotpSecret(): string {
  return base32Encode(randomBytes(20));
}

function hotp(secretBase32: string, counter: number): string {
  const key = base32Decode(secretBase32);
  const msg = Buffer.alloc(8);
  msg.writeBigUInt64BE(BigInt(counter));
  const digest = createHmac("sha1", key).update(msg).digest();
  const offset = digest[digest.length - 1]! & 0x0f;
  const code =
    ((digest[offset]! & 0x7f) << 24) |
    (digest[offset + 1]! << 16) |
    (digest[offset + 2]! << 8) |
    digest[offset + 3]!;
  return String(code % 1_000_000).padStart(6, "0");
}

export function totpCode(secretBase32: string, atMs = Date.now(), stepSeconds = 30): string {
  return hotp(secretBase32, Math.floor(atMs / 1000 / stepSeconds));
}

/**
 * Verifiera med ±1 stegs fönster (klockdrift). Konstanttidsjämförelse.
 * Returnerar det matchade steget (för engångsskydd) eller null.
 */
export function verifyTotp(
  secretBase32: string,
  code: string,
  atMs = Date.now(),
  stepSeconds = 30,
): number | null {
  const normalized = code.replace(/\s/g, "");
  if (!/^\d{6}$/.test(normalized)) return null;
  const step = Math.floor(atMs / 1000 / stepSeconds);
  for (const candidate of [step, step - 1, step + 1]) {
    const expected = hotp(secretBase32, candidate);
    if (timingSafeEqual(Buffer.from(expected), Buffer.from(normalized))) return candidate;
  }
  return null;
}

/** otpauth-URL för autentiseringsappar (Google Authenticator, 1Password …). */
export function otpauthUrl(secretBase32: string, accountName: string, issuer: string): string {
  const label = encodeURIComponent(`${issuer}:${accountName}`);
  return `otpauth://totp/${label}?secret=${secretBase32}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`;
}
