import { Buffer } from '@craftzdog/react-native-buffer';
import { NitroModules } from 'react-native-nitro-modules';
import type { Argon2 as NativeArgon2 } from './specs/argon2.nitro';
import { binaryLikeToArrayBuffer } from './utils';
import type { BinaryLike } from './utils';

let native: NativeArgon2;
function getNative(): NativeArgon2 {
  if (native == null) {
    native = NitroModules.createHybridObject<NativeArgon2>('Argon2');
  }
  return native;
}

export interface Argon2Params {
  message: BinaryLike;
  nonce: BinaryLike;
  parallelism: number;
  tagLength: number;
  memory: number;
  passes: number;
  secret?: BinaryLike;
  associatedData?: BinaryLike;
  version?: number;
}

const ARGON2_VERSION = 0x13; // v1.3

// RFC 9106 § 3.1: Argon2 input/parameter constraints.
//   p (parallelism)   1   ≤ p ≤ 2^24 - 1
//   T (tag length)    4   ≤ T ≤ 2^32 - 1
//   m (memory in KiB) 8*p ≤ m ≤ 2^32 - 1
//   t (passes)        1   ≤ t ≤ 2^32 - 1
//   |salt| (nonce)    8   ≤ |s| ≤ 2^32 - 1
//   v (version)       0x10 (v1.0) or 0x13 (v1.3)
const ARGON2_MAX_U24 = 0xff_ffff;
const ARGON2_MAX_U32 = 0xffff_ffff;

function isUInt(value: unknown, max: number): value is number {
  return (
    typeof value === 'number' &&
    Number.isFinite(value) &&
    Number.isInteger(value) &&
    value >= 0 &&
    value <= max
  );
}

function validateAlgorithm(algorithm: string): void {
  if (
    algorithm !== 'argon2d' &&
    algorithm !== 'argon2i' &&
    algorithm !== 'argon2id'
  ) {
    throw new TypeError(`Unknown argon2 algorithm: ${algorithm}`);
  }
}

// Returns the resolved nonce ArrayBuffer so the caller can pass it
// straight to native without re-resolving `params.nonce`.
function validateArgon2Params(
  params: Argon2Params,
  version: number,
): ArrayBuffer {
  if (!isUInt(params.parallelism, ARGON2_MAX_U24) || params.parallelism < 1) {
    throw new RangeError(
      `Invalid Argon2 parallelism: ${params.parallelism} ` +
        `(RFC 9106: 1 ≤ p ≤ 2^24 - 1)`,
    );
  }
  if (!isUInt(params.tagLength, ARGON2_MAX_U32) || params.tagLength < 4) {
    throw new RangeError(
      `Invalid Argon2 tagLength: ${params.tagLength} ` +
        `(RFC 9106: 4 ≤ T ≤ 2^32 - 1)`,
    );
  }
  const minMemory = 8 * params.parallelism;
  if (!isUInt(params.memory, ARGON2_MAX_U32) || params.memory < minMemory) {
    throw new RangeError(
      `Invalid Argon2 memory: ${params.memory} KiB ` +
        `(RFC 9106: 8 * p (= ${minMemory}) ≤ m ≤ 2^32 - 1)`,
    );
  }
  if (!isUInt(params.passes, ARGON2_MAX_U32) || params.passes < 1) {
    throw new RangeError(
      `Invalid Argon2 passes: ${params.passes} ` +
        `(RFC 9106: 1 ≤ t ≤ 2^32 - 1)`,
    );
  }
  if (version !== 0x10 && version !== 0x13) {
    throw new RangeError(
      `Invalid Argon2 version: 0x${version.toString(16)} ` +
        `(RFC 9106: 0x10 or 0x13)`,
    );
  }
  // Salt (nonce) must be 8..2^32 - 1 bytes — measured against the resolved
  // ArrayBuffer because BinaryLike accepts strings whose UTF-8 length is
  // what actually reaches OpenSSL.
  const nonceAB = binaryLikeToArrayBuffer(params.nonce);
  if (nonceAB.byteLength < 8 || nonceAB.byteLength > ARGON2_MAX_U32) {
    throw new RangeError(
      `Invalid Argon2 nonce length: ${nonceAB.byteLength} bytes ` +
        `(RFC 9106: 8 ≤ |s| ≤ 2^32 - 1)`,
    );
  }
  return nonceAB;
}

function toAB(value: BinaryLike): ArrayBuffer {
  return binaryLikeToArrayBuffer(value);
}

export function argon2Sync(algorithm: string, params: Argon2Params): Buffer {
  validateAlgorithm(algorithm);
  const version = params.version ?? ARGON2_VERSION;
  const nonceAB = validateArgon2Params(params, version);
  const result = getNative().hashSync(
    algorithm,
    toAB(params.message),
    nonceAB,
    params.parallelism,
    params.tagLength,
    params.memory,
    params.passes,
    version,
    params.secret ? toAB(params.secret) : undefined,
    params.associatedData ? toAB(params.associatedData) : undefined,
  );
  return Buffer.from(result);
}

export function argon2(
  algorithm: string,
  params: Argon2Params,
  callback: (err: Error | null, result: Buffer) => void,
): void {
  validateAlgorithm(algorithm);
  const version = params.version ?? ARGON2_VERSION;
  let nonceAB: ArrayBuffer;
  try {
    nonceAB = validateArgon2Params(params, version);
  } catch (err) {
    callback(err as Error, Buffer.alloc(0));
    return;
  }
  getNative()
    .hash(
      algorithm,
      toAB(params.message),
      nonceAB,
      params.parallelism,
      params.tagLength,
      params.memory,
      params.passes,
      version,
      params.secret ? toAB(params.secret) : undefined,
      params.associatedData ? toAB(params.associatedData) : undefined,
    )
    .then(ab => callback(null, Buffer.from(ab)))
    .catch((err: Error) => callback(err, Buffer.alloc(0)));
}
