/**
 * services/totp.js
 * TOTP (RFC 6238) using only Node's built-in crypto. No external deps.
 *
 * Standard 30-second window, 6 digits, SHA-1 (the spec default). Compatible
 * with Google Authenticator, Authy, 1Password, etc.
 *
 * Backup codes: 8 single-use 10-character codes, generated at enrollment,
 * shown to the user ONCE, and stored as sha256 hashes.
 */

'use strict';

const crypto = require('crypto');

const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

function base32Encode(buf) {
  let bits = 0, value = 0, out = '';
  for (let i = 0; i < buf.length; i++) {
    value = (value << 8) | buf[i];
    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;
}

function base32Decode(str) {
  str = str.replace(/=+$/, '').toUpperCase();
  let bits = 0, value = 0;
  const out = [];
  for (const ch of str) {
    const v = BASE32_ALPHABET.indexOf(ch);
    if (v < 0) continue;
    value = (value << 5) | v;
    bits += 5;
    if (bits >= 8) {
      out.push((value >>> (bits - 8)) & 0xff);
      bits -= 8;
    }
  }
  return Buffer.from(out);
}

// HOTP from RFC 4226
function hotp(secret, counter) {
  const buf = Buffer.alloc(8);
  buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0);
  buf.writeUInt32BE(counter >>> 0, 4);
  const hmac = crypto.createHmac('sha1', secret).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 (code % 1_000_000).toString().padStart(6, '0');
}

// TOTP from RFC 6238 (default 30s window, 6 digits). Accepts a base32
// string (the canonical stored form) or a Buffer.
function totp(secretB32, { time = Date.now(), period = 30, digits = 6 } = {}) {
  const secret = Buffer.isBuffer(secretB32) ? secretB32 : base32Decode(secretB32);
  const counter = Math.floor(time / 1000 / period);
  return hotp(secret, counter).slice(-digits);
}

// Allow ±1 step for clock drift. Accepts either a base32 string (the
// stored form) or a Buffer (already decoded by the caller).
function verifyToken(secretB32, token, { window = 1, period = 30 } = {}) {
  if (!token || !/^\d{6}$/.test(token)) return false;
  const secret = Buffer.isBuffer(secretB32) ? secretB32 : base32Decode(secretB32);
  const t = Math.floor(Date.now() / 1000 / period);
  for (let i = -window; i <= window; i++) {
    if (hotp(secret, t + i) === token) return true;
  }
  return false;
}

function generateSecret(bytes = 20) {
  return base32Encode(crypto.randomBytes(bytes));
}

function generateBackupCodes(n = 8) {
  const codes = [];
  for (let i = 0; i < n; i++) {
    // 10 chars, ambiguous chars removed
    const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    let s = '';
    for (let j = 0; j < 10; j++) s += alphabet[crypto.randomInt(0, alphabet.length)];
    codes.push(s);
  }
  return codes;
}

function hashBackupCode(code) {
  return crypto.createHash('sha256').update(String(code).trim().toUpperCase()).digest('hex');
}

function otpauthUrl({ secret, label, issuer }) {
  const enc = encodeURIComponent;
  return `otpauth://totp/${enc(issuer)}:${enc(label)}?secret=${enc(secret)}&issuer=${enc(issuer)}&algorithm=SHA1&digits=6&period=30`;
}

module.exports = {
  totp, verifyToken, hotp, generateSecret, generateBackupCodes,
  hashBackupCode, otpauthUrl, base32Encode, base32Decode,
};
