/* ============================================================
 * Copyright (c) 2026 RAVAPI Soluções. www.ravapi.com
 * Hash de senha — scrypt, sem dependência nova (node:crypto já tem)
 *
 * Formato: scrypt$N$r$p$salt-hex$hash-hex. Os parâmetros do custo vão
 * dentro do próprio hash — se um dia N subir (máquina mais forte, custo
 * de brute-force mais barato), hash antigo continua verificável sem
 * migração de dado nenhuma.
 * ============================================================ */

import { randomBytes, scrypt as scryptCb, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node:util';
import { DadosInvalidos } from './errors.ts';

const scrypt = promisify(scryptCb) as
  (senha: string, salt: Buffer, keylen: number, opts: { N: number; r: number; p: number }) => Promise<Buffer>;

const N = 16384, R = 8, P = 1, KEYLEN = 64;
export const TAMANHO_MINIMO_SENHA = 8;

export function validarForcaSenha(senha: string): void {
  if (!senha || senha.length < TAMANHO_MINIMO_SENHA) {
    throw new DadosInvalidos(`A senha precisa de pelo menos ${TAMANHO_MINIMO_SENHA} caracteres.`);
  }
}

export async function hashSenha(senha: string): Promise<string> {
  validarForcaSenha(senha);
  const salt = randomBytes(16);
  const hash = await scrypt(senha, salt, KEYLEN, { N, r: R, p: P });
  return `scrypt$${N}$${R}$${P}$${salt.toString('hex')}$${hash.toString('hex')}`;
}

const HEX = /^[0-9a-f]+$/i;

export async function verificarSenha(senha: string, hashArmazenado: string): Promise<boolean> {
  const partes = hashArmazenado.split('$');
  if (partes.length !== 6 || partes[0] !== 'scrypt') return false;
  const [, nStr, rStr, pStr, saltHex, hashHex] = partes;

  // Buffer.from(str, 'hex') não lança em hex inválido — só trunca (pode até
  // virar buffer vazio). Sem checar o formato antes, duas entradas inválidas
  // decodificariam pro mesmo buffer vazio e passariam no timingSafeEqual.
  if (!HEX.test(saltHex) || !HEX.test(hashHex) || saltHex.length % 2 || hashHex.length % 2) return false;
  const n = Number(nStr), r = Number(rStr), p = Number(pStr);
  if (!Number.isInteger(n) || !Number.isInteger(r) || !Number.isInteger(p)) return false;

  const salt = Buffer.from(saltHex, 'hex');
  const esperado = Buffer.from(hashHex, 'hex');
  if (esperado.length === 0) return false;

  const calculado = await scrypt(senha, salt, esperado.length, { N: n, r, p });
  return calculado.length === esperado.length && timingSafeEqual(calculado, esperado);
}
