import { Injectable } from '@nestjs/common';
import {
  randomBytes,
  scrypt,
  ScryptOptions,
  timingSafeEqual,
} from 'node:crypto';

/**
 * `promisify(scrypt)` resolve para a sobrecarga de 3 argumentos e descarta as
 * opções do KDF — o custo configurado seria silenciosamente ignorado. Daí o
 * wrapper explícito.
 */
function scryptAsync(
  password: string,
  salt: Buffer,
  keylen: number,
  options: ScryptOptions,
): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    scrypt(password, salt, keylen, options, (err, derivedKey) =>
      err ? reject(err) : resolve(derivedKey),
    );
  });
}

/**
 * Hash de senha com scrypt do core do Node.
 *
 * Por que scrypt e não bcrypt/argon2: ambos são módulos nativos com script
 * de instalação, e o npm 11 bloqueia install scripts por padrão — já
 * tropeçamos nisso com o Prisma. scrypt é KDF reconhecida, está no core e
 * elimina a dependência. Se um dia argon2id for necessário, trocar aqui é
 * suficiente: o formato do hash carrega o algoritmo.
 */
@Injectable()
export class PasswordService {
  /** Tamanho mínimo aceito. Regra do projeto, não do PRD — confirmar com o PO. */
  static readonly MIN_LENGTH = 8;

  private readonly keyLength = 64;
  private readonly cost = 2 ** 15;
  private readonly blockSize = 8;
  private readonly parallelization = 1;

  /**
   * scrypt exige `128 * N * r` bytes — 32 MB com estes parâmetros. O `maxmem`
   * padrão do Node é exatamente 32 MB e a checagem é exclusiva, então sem
   * elevar este limite o hash falha com "memory limit exceeded".
   *
   * Consequência a lembrar: são ~32 MB por hash em andamento. Cadastro e login
   * simultâneos multiplicam isso, então vale limitar concorrência antes de
   * aumentar o custo.
   */
  private readonly maxmem = 64 * 1024 * 1024;

  async hash(plain: string): Promise<string> {
    const salt = randomBytes(16);
    const derived = await scryptAsync(plain, salt, this.keyLength, {
      N: this.cost,
      r: this.blockSize,
      p: this.parallelization,
      maxmem: this.maxmem,
    });

    return [
      'scrypt',
      this.cost,
      this.blockSize,
      this.parallelization,
      salt.toString('base64'),
      derived.toString('base64'),
    ].join('$');
  }

  /** Comparação em tempo constante — nunca com `===`. */
  async verify(plain: string, stored: string): Promise<boolean> {
    const parts = stored.split('$');
    if (parts.length !== 6 || parts[0] !== 'scrypt') return false;

    const [, cost, blockSize, parallelization, saltB64, hashB64] = parts;
    const salt = Buffer.from(saltB64, 'base64');
    const expected = Buffer.from(hashB64, 'base64');

    const derived = await scryptAsync(plain, salt, expected.length, {
      N: Number(cost),
      r: Number(blockSize),
      p: Number(parallelization),
      maxmem: this.maxmem,
    });

    return (
      derived.length === expected.length && timingSafeEqual(derived, expected)
    );
  }
}
