import crypto from 'node:crypto';
import { promisify } from 'node:util';

const MD5_CRYPT_ALPHABET = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const scryptAsync = promisify(crypto.scrypt);
const dummyScryptSalt = 'mailhub-dummy-auth-v1';
const dummyScryptHash = `scrypt$${dummyScryptSalt}$${crypto
  .scryptSync('mailhub-dummy-password', dummyScryptSalt, 64)
  .toString('hex')}`;

export function hashPassword(password) {
  const salt = crypto.randomBytes(16).toString('hex');
  const hash = crypto.scryptSync(String(password), salt, 64).toString('hex');
  return `scrypt$${salt}$${hash}`;
}

export async function hashPasswordAsync(password) {
  const salt = crypto.randomBytes(16).toString('hex');
  const hash = await scryptAsync(String(password), salt, 64);
  return `scrypt$${salt}$${Buffer.from(hash).toString('hex')}`;
}

export function verifyPassword(password, stored) {
  if (isLegacyPasswordHash(stored)) return verifyVestaPassword(password, stored);
  return verifyScryptPassword(password, stored);
}

export async function verifyPasswordAsync(password, stored) {
  if (isLegacyPasswordHash(stored)) return verifyVestaPassword(password, stored);
  return verifyScryptPasswordAsync(password, stored);
}

export function verifyScryptPassword(password, stored) {
  const parts = String(stored || '').split('$');
  if (parts.length !== 3) return false;
  const [scheme, salt, expectedHex] = parts;
  if (scheme !== 'scrypt' || !salt || !/^[0-9a-f]{128}$/i.test(expectedHex)) return false;
  const actual = crypto.scryptSync(String(password), salt, 64);
  return safeEqual(actual, Buffer.from(expectedHex, 'hex'));
}

export async function verifyScryptPasswordAsync(password, stored) {
  const parts = String(stored || '').split('$');
  if (parts.length !== 3) return false;
  const [scheme, salt, expectedHex] = parts;
  if (scheme !== 'scrypt' || !salt || !/^[0-9a-f]{128}$/i.test(expectedHex)) return false;
  const actual = await scryptAsync(String(password), salt, 64);
  return safeEqual(Buffer.from(actual), Buffer.from(expectedHex, 'hex'));
}

export function consumeDummyPasswordVerification(password) {
  verifyScryptPassword(password, dummyScryptHash);
  return false;
}

export async function consumeDummyPasswordVerificationAsync(password) {
  await verifyScryptPasswordAsync(password, dummyScryptHash);
  return false;
}

export function isLegacyPasswordHash(stored) {
  return Boolean(parseVestaPasswordHash(stored));
}

export function verifyVestaPassword(password, stored) {
  const parsed = parseVestaPasswordHash(stored);
  if (!parsed) return false;
  const actual = md5Crypt(String(password), parsed.salt);
  return safeEqual(Buffer.from(actual), Buffer.from(parsed.hash));
}

function parseVestaPasswordHash(stored) {
  const match = String(stored || '').match(
    /^(?:\{(?:MD5|MD5-CRYPT)\})?(\$1\$([./0-9A-Za-z]{1,8})\$[./0-9A-Za-z]{22})$/i
  );
  if (!match) return null;
  return { hash: match[1], salt: match[2] };
}

function md5Crypt(password, salt) {
  const passwordBytes = Buffer.from(password, 'utf8');
  const saltBytes = Buffer.from(salt, 'ascii');
  const magicBytes = Buffer.from('$1$', 'ascii');
  const initialParts = [passwordBytes, magicBytes, saltBytes];
  const alternate = md5([passwordBytes, saltBytes, passwordBytes]);

  for (let remaining = passwordBytes.length; remaining > 0; remaining -= 16) {
    initialParts.push(alternate.subarray(0, Math.min(16, remaining)));
  }
  for (let remaining = passwordBytes.length; remaining > 0; remaining >>= 1) {
    initialParts.push(remaining & 1 ? Buffer.from([0]) : passwordBytes.subarray(0, 1));
  }

  let digest = md5(initialParts);
  for (let index = 0; index < 1000; index += 1) {
    const roundParts = [index & 1 ? passwordBytes : digest];
    if (index % 3 !== 0) roundParts.push(saltBytes);
    if (index % 7 !== 0) roundParts.push(passwordBytes);
    roundParts.push(index & 1 ? digest : passwordBytes);
    digest = md5(roundParts);
  }

  return `$1$${salt}$${encodeMd5CryptDigest(digest)}`;
}

function md5(parts) {
  const hash = crypto.createHash('md5');
  for (const part of parts) hash.update(part);
  return hash.digest();
}

function encodeMd5CryptDigest(digest) {
  return [
    cryptBase64(digest[0], digest[6], digest[12], 4),
    cryptBase64(digest[1], digest[7], digest[13], 4),
    cryptBase64(digest[2], digest[8], digest[14], 4),
    cryptBase64(digest[3], digest[9], digest[15], 4),
    cryptBase64(digest[4], digest[10], digest[5], 4),
    cryptBase64(0, 0, digest[11], 2)
  ].join('');
}

function cryptBase64(high, middle, low, length) {
  let value = (high << 16) | (middle << 8) | low;
  let result = '';
  for (let index = 0; index < length; index += 1) {
    result += MD5_CRYPT_ALPHABET[value & 0x3f];
    value >>>= 6;
  }
  return result;
}

function safeEqual(actual, expected) {
  const length = Math.max(actual.length, expected.length);
  const actualPadded = Buffer.alloc(length);
  const expectedPadded = Buffer.alloc(length);
  actual.copy(actualPadded);
  expected.copy(expectedPadded);
  return crypto.timingSafeEqual(actualPadded, expectedPadded) && actual.length === expected.length;
}
