// TOTP (RFC 6238) generator for k6.
//
// Used by journeys/totp_full.js: parse the otpauth_url returned by
// /me/totp/enroll-start, then compute a 6-digit code for the current 30s window.
//
// k6 ships HMAC-SHA1 in k6/crypto so no xk6 extension is required.
import crypto from 'k6/crypto';

const BASE32_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

// base32Decode returns a Uint8Array for a (possibly padded) base32 string.
// Accepts mixed case and ignores '=' padding and whitespace.
export function base32Decode(input) {
  if (!input) return new Uint8Array(0);
  let cleaned = '';
  for (let i = 0; i < input.length; i++) {
    const c = input[i].toUpperCase();
    if (c === '=' || c === ' ' || c === '\n' || c === '\r' || c === '\t') continue;
    cleaned += c;
  }
  const out = [];
  let bits = 0;
  let value = 0;
  for (let i = 0; i < cleaned.length; i++) {
    const idx = BASE32_ALPHA.indexOf(cleaned[i]);
    if (idx < 0) throw new Error(`base32: invalid char '${cleaned[i]}'`);
    value = (value << 5) | idx;
    bits += 5;
    if (bits >= 8) {
      bits -= 8;
      out.push((value >> bits) & 0xff);
    }
  }
  return new Uint8Array(out);
}

// parseOTPAuth extracts the base32 secret + algo/digits/period from an
// otpauth://totp/... URL. Returns defaults (SHA1, 6 digits, 30s period) for
// fields not present. Throws if `secret` is missing.
export function parseOTPAuth(url) {
  if (!url || url.indexOf('otpauth://totp/') !== 0) {
    throw new Error(`parseOTPAuth: not an otpauth totp url: ${url}`);
  }
  const q = url.indexOf('?');
  if (q < 0) throw new Error('parseOTPAuth: missing query string');
  const query = url.slice(q + 1);
  const params = {};
  for (const part of query.split('&')) {
    const eq = part.indexOf('=');
    if (eq < 0) continue;
    const k = decodeURIComponent(part.slice(0, eq));
    const v = decodeURIComponent(part.slice(eq + 1));
    params[k.toLowerCase()] = v;
  }
  if (!params.secret) throw new Error('parseOTPAuth: missing secret param');
  return {
    secret: params.secret,
    algorithm: (params.algorithm || 'SHA1').toUpperCase(),
    digits: parseInt(params.digits || '6', 10),
    period: parseInt(params.period || '30', 10),
  };
}

// hotp computes an HOTP code as per RFC 4226 given a key (Uint8Array) and a
// 64-bit counter. Returns a digits-long zero-padded string.
function hotp(keyBytes, counter, digits) {
  // Build 8-byte big-endian counter buffer
  const buf = new ArrayBuffer(8);
  const view = new DataView(buf);
  // Counter can exceed Number.MAX_SAFE_INTEGER theoretically but for unix-time
  // counters until year ~9999 it fits comfortably in a 32-bit low half.
  const high = Math.floor(counter / 0x100000000);
  const low = counter >>> 0;
  view.setUint32(0, high, false);
  view.setUint32(4, low, false);

  // hmac with binary output gives us an ArrayBuffer of 20 bytes for SHA1
  const macBuf = crypto.hmac('sha1', keyBytes.buffer, buf, 'binary');
  const mac = new Uint8Array(macBuf);
  const offset = mac[mac.length - 1] & 0x0f;
  const bin =
    ((mac[offset] & 0x7f) << 24) |
    ((mac[offset + 1] & 0xff) << 16) |
    ((mac[offset + 2] & 0xff) << 8) |
    (mac[offset + 3] & 0xff);
  const mod = Math.pow(10, digits);
  return String(bin % mod).padStart(digits, '0');
}

// generateTOTP returns the current TOTP code for the given otpauth secret.
// Pass timeOverrideMs to test specific windows; defaults to Date.now().
export function generateTOTP(otpauthURLOrSecret, timeOverrideMs) {
  let opts;
  if (typeof otpauthURLOrSecret === 'string' && otpauthURLOrSecret.indexOf('otpauth://') === 0) {
    opts = parseOTPAuth(otpauthURLOrSecret);
  } else {
    opts = { secret: otpauthURLOrSecret, algorithm: 'SHA1', digits: 6, period: 30 };
  }
  if (opts.algorithm !== 'SHA1') {
    throw new Error(`generateTOTP: only SHA1 supported, got ${opts.algorithm}`);
  }
  const key = base32Decode(opts.secret);
  const t = typeof timeOverrideMs === 'number' ? timeOverrideMs : Date.now();
  const counter = Math.floor(t / 1000 / opts.period);
  return hotp(key, counter, opts.digits);
}
