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

/**
 * Time-based one-time passwords (RFC 6238) and recovery codes (2026-09-22).
 * Pure Node crypto — no dependency. Works with Google Authenticator, Authy,
 * 1Password, Microsoft Authenticator (SHA-1, 6 digits, 30-second steps).
 */

const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'

export function base32Encode(bytes: Uint8Array): string {
  let bits = 0, value = 0, out = ''
  for (const b of bytes) {
    value = (value << 8) | b
    bits += 8
    while (bits >= 5) { out += ALPHABET[(value >>> (bits - 5)) & 31]; bits -= 5 }
  }
  if (bits > 0) out += ALPHABET[(value << (5 - bits)) & 31]
  return out
}

export function base32Decode(s: string): Uint8Array {
  const clean = s.toUpperCase().replace(/[^A-Z2-7]/g, '')
  let bits = 0, value = 0
  const out: number[] = []
  for (const c of clean) {
    value = (value << 5) | ALPHABET.indexOf(c)
    bits += 5
    if (bits >= 8) { out.push((value >>> (bits - 8)) & 255); bits -= 8 }
  }
  return Uint8Array.from(out)
}

/** 160-bit secret, base32 (what the authenticator app stores). */
export function generateTotpSecret(): string {
  return base32Encode(randomBytes(20))
}

export function totpCode(secretBase32: string, timeMs = Date.now(), step = 30, digits = 6): string {
  const counter = Math.floor(timeMs / 1000 / step)
  const buf = Buffer.alloc(8)
  buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0)
  buf.writeUInt32BE(counter >>> 0, 4)
  const hmac = createHmac('sha1', Buffer.from(base32Decode(secretBase32))).update(buf).digest()
  const offset = hmac[hmac.length - 1] & 0xf
  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')
}

/** Accepts the current step and one step either side (clock drift). */
export function verifyTotp(secretBase32: string, code: string, timeMs = Date.now()): boolean {
  const given = code.replace(/\s+/g, '')
  if (!/^\d{6}$/.test(given)) return false
  for (const drift of [0, -1, 1]) {
    const expected = totpCode(secretBase32, timeMs + drift * 30_000)
    if (timingSafeEqual(Buffer.from(expected), Buffer.from(given))) return true
  }
  return false
}

export function otpauthUri(params: { issuer: string; account: string; secret: string }): string {
  const label = encodeURIComponent(`${params.issuer}:${params.account}`)
  return `otpauth://totp/${label}?secret=${params.secret}&issuer=${encodeURIComponent(params.issuer)}&algorithm=SHA1&digits=6&period=30`
}

/* ---------------- recovery codes ---------------- */

const RECOVERY_ALPHABET = 'abcdefghjkmnpqrstuvwxyz23456789' // no 0/o/1/i/l

/** Eight single-use codes like "k7m2-p9qx-3n4w". Only their hashes are stored. */
export function generateRecoveryCodes(count = 8): string[] {
  return Array.from({ length: count }, () => {
    const bytes = randomBytes(12)
    const chars = [...bytes].map((b) => RECOVERY_ALPHABET[b % RECOVERY_ALPHABET.length]).join('')
    return `${chars.slice(0, 4)}-${chars.slice(4, 8)}-${chars.slice(8, 12)}`
  })
}

export function hashRecoveryCode(code: string): string {
  return createHash('sha256').update(code.toLowerCase().replace(/[^a-z0-9]/g, '')).digest('hex')
}

/** Index of the matching stored hash, or -1. */
export function findRecoveryCode(code: string, hashes: string[]): number {
  const h = hashRecoveryCode(code)
  return hashes.findIndex((x) => x.length === h.length && timingSafeEqual(Buffer.from(x), Buffer.from(h)))
}
