import crypto from 'node:crypto'
import type { RowDataPacket } from 'mysql2'
import { db, DB } from './db'

/**
 * TOTP para el login del juego (Battle.net / AzerothCore 3.4.3).
 *
 * El secreto se guarda en `acore_auth.account.totp_secret` (VARBINARY). El
 * servidor lo lee y valida en cada inicio de sesión. Reproduce exactamente la
 * implementación del core (src/common/Cryptography/TOTP.cpp): HMAC-SHA1, ventana
 * de 30 s, 6 dígitos y una tolerancia de ±1 intervalo.
 *
 * Almacenamiento (igual que `.account 2fa setup` del core):
 *  - Sin master key (por defecto, TOTPMasterSecret vacío): se guarda el secreto
 *    en crudo (20 bytes).
 *  - Con master key (TOTP_MASTER_SECRET = mismo hex que TOTPMasterSecret del
 *    bnetserver): se cifra con AES-128-GCM y se guarda `ciphertext‖IV(12)‖tag(12)`.
 */

const SECRET_LENGTH = 20
const INTERVAL = 30

/* --------------------------------- Base32 ---------------------------------- */
const B32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'

export function base32Encode(buf: Buffer): string {
  let bits = 0
  let value = 0
  let out = ''
  for (const byte of buf) {
    value = (value << 8) | byte
    bits += 8
    while (bits >= 5) {
      out += B32_ALPHABET[(value >>> (bits - 5)) & 31]
      bits -= 5
    }
  }
  if (bits > 0) out += B32_ALPHABET[(value << (5 - bits)) & 31]
  return out // sin relleno '='; los autenticadores lo aceptan
}

/* ---------------------------------- TOTP ----------------------------------- */
/** Genera el token de 6 dígitos para un instante dado (mismo algoritmo que el core). */
export function generateToken(secret: Buffer, timestamp: number): number {
  let counter = Math.floor(timestamp / INTERVAL)
  const challenge = Buffer.alloc(8)
  for (let i = 7; i >= 0; i--) {
    challenge[i] = counter & 0xff
    counter = Math.floor(counter / 256)
  }
  const digest = crypto.createHmac('sha1', secret).update(challenge).digest()
  const offset = digest[19] & 0xf
  const truncated =
    (((digest[offset] & 0x7f) << 24) |
      (digest[offset + 1] << 16) |
      (digest[offset + 2] << 8) |
      digest[offset + 3]) >>>
    0
  return truncated % 1000000
}

/** Valida un token con tolerancia de ±1 intervalo (now-30, now, now+30). */
export function validateToken(secret: Buffer, token: number): boolean {
  const now = Math.floor(Date.now() / 1000)
  return (
    token === generateToken(secret, now - INTERVAL) ||
    token === generateToken(secret, now) ||
    token === generateToken(secret, now + INTERVAL)
  )
}

/* -------------------------------- otpauth ---------------------------------- */
export function otpauthUrl(issuer: string, label: string, secretBase32: string): string {
  const iss = encodeURIComponent(issuer)
  const lbl = encodeURIComponent(label)
  return `otpauth://totp/${iss}:${lbl}?secret=${secretBase32}&issuer=${iss}&algorithm=SHA1&digits=6&period=30`
}

/* ------------------------ Cifrado para la base de datos --------------------- */
// Clave AES de 16 bytes desde el master key hex, en little-endian (igual que
// BigNumber::ToByteArray del core, cuyo valor por defecto es littleEndian=true).
function masterKeyBytes(): Buffer | null {
  const hex = (process.env.TOTP_MASTER_SECRET || '').trim()
  if (!hex) return null
  let n: bigint
  try {
    n = BigInt('0x' + hex.replace(/^0x/i, ''))
  } catch {
    return null
  }
  const buf = Buffer.alloc(16)
  let v = n
  for (let i = 0; i < 16; i++) {
    buf[i] = Number(v & 0xffn)
    v >>= 8n
  }
  return buf
}

/** Codifica el secreto para guardarlo en `account.totp_secret` (crudo o AES-GCM). */
export function encodeSecretForDb(raw: Buffer): Buffer {
  const key = masterKeyBytes()
  if (!key) return raw
  const iv = crypto.randomBytes(12)
  const cipher = crypto.createCipheriv('aes-128-gcm', key, iv, { authTagLength: 12 })
  const ct = Buffer.concat([cipher.update(raw), cipher.final()])
  const tag = cipher.getAuthTag() // 12 bytes
  return Buffer.concat([ct, iv, tag]) // ciphertext ‖ IV(12) ‖ tag(12)
}

/** Decodifica el secreto almacenado (crudo o AES-GCM). Devuelve null si falla. */
export function decodeSecretFromDb(stored: Buffer): Buffer | null {
  const key = masterKeyBytes()
  if (!key) return stored
  if (stored.length < 24) return null
  const tag = stored.subarray(stored.length - 12)
  const iv = stored.subarray(stored.length - 24, stored.length - 12)
  const ct = stored.subarray(0, stored.length - 24)
  try {
    const decipher = crypto.createDecipheriv('aes-128-gcm', key, iv, { authTagLength: 12 })
    decipher.setAuthTag(tag)
    return Buffer.concat([decipher.update(ct), decipher.final()])
  } catch {
    return null
  }
}

/* --------------------------- Acceso a la cuenta ---------------------------- */
export async function is2faEnabled(accountId: number): Promise<boolean> {
  const [rows] = await db(DB.auth).query<RowDataPacket[]>(
    'SELECT totp_secret FROM account WHERE id = ?',
    [accountId],
  )
  return Boolean(rows[0] && rows[0].totp_secret != null)
}

/** Devuelve el secreto TOTP en crudo (descifrado) de la cuenta, o null si no tiene 2FA. */
export async function getAccountTotpSecret(accountId: number): Promise<Buffer | null> {
  const [rows] = await db(DB.auth).query<RowDataPacket[]>(
    'SELECT totp_secret FROM account WHERE id = ?',
    [accountId],
  )
  const stored = rows[0]?.totp_secret
  if (stored == null) return null
  return decodeSecretFromDb(Buffer.from(stored))
}

export async function setAccountTotpSecret(accountId: number, raw: Buffer): Promise<void> {
  await db(DB.auth).query('UPDATE account SET totp_secret = ? WHERE id = ?', [encodeSecretForDb(raw), accountId])
}

export async function clearAccountTotpSecret(accountId: number): Promise<void> {
  await db(DB.auth).query('UPDATE account SET totp_secret = NULL WHERE id = ?', [accountId])
}

/** Genera un secreto TOTP aleatorio (20 bytes, como RECOMMENDED_SECRET_LENGTH del core). */
export function generateSecret(): Buffer {
  return crypto.randomBytes(SECRET_LENGTH)
}
