import 'dart:convert';
import 'dart:math';

import 'package:cryptography/cryptography.dart';

import '../../application/application.dart';
import '../../domain/domain.dart';

final class Pbkdf2PasswordHasher implements PasswordHasher {
  Pbkdf2PasswordHasher({
    this.iterations = 210000,
    this.saltLength = 16,
    this.bits = 256,
    Random? random,
  }) : _random = random ?? Random.secure();

  final int iterations;
  final int saltLength;
  final int bits;
  final Random _random;

  @override
  Future<String> hash(String password) async {
    final salt = List<int>.generate(saltLength, (_) => _random.nextInt(256));
    final hash = await _derive(password, salt, iterations, bits);
    return [
      'pbkdf2_sha256',
      iterations.toString(),
      base64UrlEncode(salt),
      base64UrlEncode(hash),
    ].join(r'$');
  }

  @override
  Future<bool> verify({
    required String password,
    required String passwordHash,
  }) async {
    final parts = passwordHash.split(r'$');
    if (parts.length != 4 || parts[0] != 'pbkdf2_sha256') {
      throw const ValidationException('Unsupported password hash format.');
    }

    final parsedIterations = int.tryParse(parts[1]);
    if (parsedIterations == null || parsedIterations <= 0) {
      throw const ValidationException('Invalid password hash iterations.');
    }

    final salt = base64Url.decode(parts[2]);
    final expectedHash = base64Url.decode(parts[3]);
    final actualHash = await _derive(
      password,
      salt,
      parsedIterations,
      expectedHash.length * 8,
    );
    return _constantTimeEquals(actualHash, expectedHash);
  }

  Future<List<int>> _derive(
    String password,
    List<int> salt,
    int iterations,
    int bits,
  ) async {
    final algorithm = Pbkdf2(
      macAlgorithm: Hmac.sha256(),
      iterations: iterations,
      bits: bits,
    );
    final key = await algorithm.deriveKey(
      secretKey: SecretKey(utf8.encode(password)),
      nonce: salt,
    );
    return key.extractBytes();
  }
}

bool _constantTimeEquals(List<int> left, List<int> right) {
  if (left.length != right.length) {
    return false;
  }
  var diff = 0;
  for (var index = 0; index < left.length; index++) {
    diff |= left[index] ^ right[index];
  }
  return diff == 0;
}
