/**
 * RFC 6238 TOTP（Worker 侧计算，依赖 Web Crypto HMAC）
 */

const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

export function normalizeBase32Secret(secret: string): string {
  return secret.replace(/\s+/g, '').toUpperCase().replace(/=+$/, '');
}

export function decodeBase32(s: string): Uint8Array {
  const str = normalizeBase32Secret(s);
  let bits = 0;
  let value = 0;
  const output: number[] = [];
  for (let i = 0; i < str.length; i++) {
    const idx = BASE32_ALPHABET.indexOf(str[i]!);
    if (idx === -1) continue;
    value = (value << 5) | idx;
    bits += 5;
    if (bits >= 8) {
      output.push((value >>> (bits - 8)) & 0xff);
      bits -= 8;
    }
  }
  return new Uint8Array(output);
}

function writeBigEndianCounter(view: DataView, counter: number): void {
  let c = counter;
  for (let i = 7; i >= 0; i--) {
    view.setUint8(i, c & 0xff);
    c = Math.floor(c / 256);
  }
}

async function hmacDigest(
  keyBytes: Uint8Array,
  message: ArrayBuffer,
  hash: 'SHA-1' | 'SHA-256' | 'SHA-512',
): Promise<Uint8Array> {
  const cryptoKey = await crypto.subtle.importKey(
    'raw',
    keyBytes as BufferSource,
    { name: 'HMAC', hash },
    false,
    ['sign'],
  );
  return new Uint8Array(await crypto.subtle.sign('HMAC', cryptoKey, message));
}

export async function computeTotpCode(
  secretBase32: string,
  options: { digits: number; period: number; algorithm: string; nowMs?: number },
): Promise<{ code: string; expiresInSeconds: number }> {
  const now = options.nowMs ?? Date.now();
  const period = options.period || 30;
  const digits = Math.min(10, Math.max(6, options.digits || 6));
  const algo = (options.algorithm || 'SHA1').toUpperCase();
  const hashName =
    algo === 'SHA256' || algo === 'SHA-256'
      ? 'SHA-256'
      : algo === 'SHA512' || algo === 'SHA-512'
        ? 'SHA-512'
        : 'SHA-1';

  const keyBytes = decodeBase32(secretBase32);
  if (keyBytes.length === 0) {
    throw new Error('密钥无效');
  }

  const counter = Math.floor(now / 1000 / period);
  const buf = new ArrayBuffer(8);
  writeBigEndianCounter(new DataView(buf), counter);
  const hmac = await hmacDigest(keyBytes, buf, hashName);
  const offset = hmac[hmac.length - 1]! & 0x0f;
  const bin =
    ((hmac[offset]! & 0x7f) << 24) |
    ((hmac[offset + 1]! & 0xff) << 16) |
    ((hmac[offset + 2]! & 0xff) << 8) |
    (hmac[offset + 3]! & 0xff);
  const mod = 10 ** digits;
  const num = bin % mod;
  const code = num.toString(10).padStart(digits, '0');

  const elapsed = Math.floor(now / 1000) % period;
  const expiresInSeconds = period - elapsed;

  return { code, expiresInSeconds };
}
