import { createHmac, timingSafeEqual } from 'crypto';

const decodeBase32 = (value: string): Buffer => {
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  const normalized = value.toUpperCase().replace(/=|\s|-/g, '');
  let bits = '';
  for (const character of normalized) {
    const index = alphabet.indexOf(character);
    if (index < 0) return Buffer.alloc(0);
    bits += index.toString(2).padStart(5, '0');
  }
  const bytes: number[] = [];
  for (let offset = 0; offset + 8 <= bits.length; offset += 8) {
    bytes.push(Number.parseInt(bits.slice(offset, offset + 8), 2));
  }
  return Buffer.from(bytes);
};

const generateTotp = (secret: Buffer, counter: number): string => {
  const message = Buffer.alloc(8);
  message.writeBigUInt64BE(BigInt(counter));
  const digest = createHmac('sha1', secret).update(message).digest();
  const offset = digest[digest.length - 1] & 0x0f;
  return ((digest.readUInt32BE(offset) & 0x7fffffff) % 1_000_000)
    .toString()
    .padStart(6, '0');
};

export const verifyTotp = (base32Secret: string, suppliedCode: string, nowMs = Date.now()): boolean => {
  if (!/^\d{6}$/.test(suppliedCode)) return false;
  const secret = decodeBase32(base32Secret);
  if (!secret.length) return false;
  const counter = Math.floor(nowMs / 30_000);
  return [-1, 0, 1].filter((drift) => counter + drift >= 0).some((drift) => {
    const expected = Buffer.from(generateTotp(secret, counter + drift));
    const supplied = Buffer.from(suppliedCode);
    return expected.length === supplied.length && timingSafeEqual(expected, supplied);
  });
};
