import { Buffer } from '@craftzdog/react-native-buffer';
import { NitroModules } from 'react-native-nitro-modules';
import type { BinaryLike } from './utils';
import {
  HashContext,
  binaryLikeToArrayBuffer,
  bufferLikeToArrayBuffer,
  lazyDOMException,
  normalizeHashName,
} from './utils';
import type { SubtleAlgorithm } from './utils';
import type { Pbkdf2 } from './specs/pbkdf2.nitro';
import { promisify } from 'util';
import type { CryptoKey } from './keys';

const WRONG_PASS =
  'Password must be a string, a Buffer, a typed array or a DataView';
const WRONG_SALT = `Salt must be a string, a Buffer, a typed array or a DataView`;

type Password = BinaryLike;
type Salt = BinaryLike;
type Pbkdf2Callback = (err: Error | null, derivedKey?: Buffer) => void;

// to use native bits in sub-functions, use getNative(). don't call it at top-level!
let native: Pbkdf2;
function getNative(): Pbkdf2 {
  if (native == null) {
    // lazy-load the Nitro HybridObject
    native = NitroModules.createHybridObject<Pbkdf2>('Pbkdf2');
  }
  return native;
}

const MAX_INT32 = 2147483647;

function validateParameters(iterations: number, keylen: number): void {
  if (typeof iterations !== 'number') {
    throw new TypeError('Iterations not a number');
  }
  if (typeof keylen !== 'number') {
    throw new TypeError('Key length not a number');
  }
  if (
    iterations < 1 ||
    !Number.isFinite(iterations) ||
    !Number.isInteger(iterations) ||
    iterations > MAX_INT32
  ) {
    throw new TypeError('Bad iterations');
  }
  if (
    keylen < 0 ||
    !Number.isFinite(keylen) ||
    !Number.isInteger(keylen) ||
    keylen > MAX_INT32
  ) {
    throw new TypeError('Bad key length');
  }
}

function sanitizeInput(input: BinaryLike, errorMsg: string): ArrayBuffer {
  try {
    return binaryLikeToArrayBuffer(input);
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
  } catch (_e: unknown) {
    throw new Error(errorMsg);
  }
}

export function pbkdf2(
  password: Password,
  salt: Salt,
  iterations: number,
  keylen: number,
  digest: string,
  callback: Pbkdf2Callback,
): void {
  if (callback === undefined || typeof callback !== 'function') {
    throw new Error('No callback provided to pbkdf2');
  }
  validateParameters(iterations, keylen);
  const sanitizedPassword = sanitizeInput(password, WRONG_PASS);
  const sanitizedSalt = sanitizeInput(salt, WRONG_SALT);
  const normalizedDigest = normalizeHashName(digest, HashContext.Node);

  getNative();
  native
    .pbkdf2(
      sanitizedPassword,
      sanitizedSalt,
      iterations,
      keylen,
      normalizedDigest,
    )
    .then(
      (res: ArrayBuffer) => {
        callback!(null, Buffer.from(res));
      },
      (e: Error) => {
        callback!(e);
      },
    );
}

export function pbkdf2Sync(
  password: Password,
  salt: Salt,
  iterations: number,
  keylen: number,
  digest: string,
): Buffer {
  validateParameters(iterations, keylen);
  const sanitizedPassword = sanitizeInput(password, WRONG_PASS);
  const sanitizedSalt = sanitizeInput(salt, WRONG_SALT);

  const algo = normalizeHashName(digest, HashContext.Node);
  getNative();
  const result: ArrayBuffer = native.pbkdf2Sync(
    sanitizedPassword,
    sanitizedSalt,
    iterations,
    keylen,
    algo,
  );

  return Buffer.from(result);
}

// We need this because the typescript  overload signatures in pbkdf2() above do
// not play nice with promisify() below.
const pbkdf2WithDigest = (
  password: Password,
  salt: Salt,
  iterations: number,
  keylen: number,
  digest: string,
  callback: Pbkdf2Callback,
) => pbkdf2(password, salt, iterations, keylen, digest, callback);

const pbkdf2Promise = promisify(pbkdf2WithDigest);
export async function pbkdf2DeriveBits(
  algorithm: SubtleAlgorithm,
  baseKey: CryptoKey,
  length: number,
): Promise<ArrayBuffer> {
  const { iterations, hash, salt } = algorithm;
  const normalizedHash = normalizeHashName(hash);
  if (!normalizedHash) {
    throw lazyDOMException('hash cannot be blank', 'OperationError');
  }
  if (!iterations || iterations === 0) {
    throw lazyDOMException('iterations cannot be zero', 'OperationError');
  }
  if (!salt) {
    throw lazyDOMException(WRONG_SALT, 'OperationError');
  }
  const raw = baseKey.keyObject.export();

  if (length === 0)
    throw lazyDOMException('length cannot be zero', 'OperationError');
  if (length === null)
    throw lazyDOMException('length cannot be null', 'OperationError');
  if (length % 8) {
    throw lazyDOMException('length must be a multiple of 8', 'OperationError');
  }

  const sanitizedPassword = sanitizeInput(raw, WRONG_PASS);
  const sanitizedSalt = sanitizeInput(salt, WRONG_SALT);
  const result: Buffer | undefined = await pbkdf2Promise(
    sanitizedPassword,
    sanitizedSalt,
    iterations,
    length / 8,
    normalizedHash,
  );
  if (!result) {
    throw lazyDOMException(
      'received bad result from pbkdf2()',
      'OperationError',
    );
  }
  return bufferLikeToArrayBuffer(result);
}
