// Copyright 2018-2026 the Deno authors. MIT license.
// Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license.

// deno-lint-ignore-file no-explicit-any

(function () {
const { core, primordials } = __bootstrap;
const {
  Array,
  ArrayBufferPrototypeGetByteLength,
  BigInt,
  BigIntPrototypeToString,
  DataView,
  DataViewPrototypeGetBuffer,
  DataViewPrototypeGetByteLength,
  DataViewPrototypeGetByteOffset,
  DataViewPrototypeGetUint8,
  MathClz32,
  NumberParseInt,
  ObjectPrototypeIsPrototypeOf,
  PromisePrototypeCatch,
  PromisePrototypeThen,
  SafeArrayIterator,
  StringFromCharCode,
  StringPrototypePadStart,
  StringPrototypeSubstring,
  TypedArrayPrototypeGetBuffer,
  TypedArrayPrototypeGetByteLength,
  TypedArrayPrototypeGetByteOffset,
  Uint8Array,
  Uint8ArrayPrototype,
} = primordials;
const {
  op_node_check_prime_bytes,
  op_node_check_prime_bytes_async,
  op_node_gen_prime,
  op_node_gen_prime_async,
} = core.ops;

const { default: randomBytes } = core.loadExtScript(
  "ext:deno_node/internal/crypto/_randomBytes.ts",
);
const { default: randomFill, randomFillSync } = core.loadExtScript(
  "ext:deno_node/internal/crypto/_randomFill.mjs",
);
const { default: randomInt } = core.loadExtScript(
  "ext:deno_node/internal/crypto/_randomInt.ts",
);
const {
  validateBoolean,
  validateFunction,
  validateInt32,
  validateObject,
} = core.loadExtScript("ext:deno_node/internal/validators.mjs");
const {
  isAnyArrayBuffer,
  isArrayBufferView,
  isTypedArray,
} = core.loadExtScript("ext:deno_node/internal/util/types.ts");
const {
  ERR_INVALID_ARG_TYPE,
  ERR_OUT_OF_RANGE,
  NodeError,
  NodeRangeError,
} = core.loadExtScript("ext:deno_node/internal/errors.ts");
const { Buffer } = core.loadExtScript("ext:deno_node/internal/buffer.mjs");

// OpenSSL BIGNUM max size: INT_MAX / (4 * BN_BITS2) words * 8 bytes/word
// On 64-bit: (2^31 - 1) / 256 * 8 = 67108856 bytes
const OPENSSL_BIGNUM_MAX_BYTES = (((2 ** 31 - 1) / (4 * 64)) | 0) * 8;

function checkPrime(
  candidate: any,
  options: any = { __proto__: null },
  callback?: (err: Error | null, result: boolean) => void,
) {
  if (typeof options === "function") {
    callback = options;
    options = {};
  }

  validateFunction(callback, "callback");
  validateObject(options, "options");

  const {
    checks = 0,
  } = options!;

  validateInt32(checks, "options.checks", 0);

  let candidateBytes: ArrayBufferView | ArrayBuffer;
  if (typeof candidate === "bigint") {
    if (candidate < 0) {
      throw new ERR_OUT_OF_RANGE("candidate", ">= 0", candidate);
    }
    candidateBytes = bigintToBytes(candidate);
  } else if (isAnyArrayBuffer(candidate) || isArrayBufferView(candidate)) {
    const byteLength = isArrayBufferView(candidate)
      ? arrayBufferViewByteLength(candidate as ArrayBufferView)
      : ArrayBufferPrototypeGetByteLength(candidate as ArrayBuffer);
    if (byteLength > OPENSSL_BIGNUM_MAX_BYTES) {
      throw new NodeError(
        "ERR_OSSL_BN_BIGNUM_TOO_LONG",
        "bignum too long",
      );
    }
    candidateBytes = candidate;
  } else {
    throw new ERR_INVALID_ARG_TYPE(
      "candidate",
      [
        "ArrayBuffer",
        "TypedArray",
        "Buffer",
        "DataView",
        "bigint",
      ],
      candidate,
    );
  }

  PromisePrototypeCatch(
    PromisePrototypeThen(
      op_node_check_prime_bytes_async(candidateBytes, checks),
      (result) => {
        callback?.(null, result);
      },
    ),
    (err) => {
      callback?.(err, false);
    },
  );
}

function checkPrimeSync(
  candidate: any,
  options: any = { __proto__: null },
): boolean {
  validateObject(options, "options");

  const {
    checks = 0,
  } = options!;

  validateInt32(checks, "options.checks", 0);

  let candidateBytes: ArrayBufferView | ArrayBuffer;
  if (typeof candidate === "bigint") {
    if (candidate < 0) {
      throw new ERR_OUT_OF_RANGE("candidate", ">= 0", candidate);
    }
    candidateBytes = bigintToBytes(candidate);
  } else if (isAnyArrayBuffer(candidate) || isArrayBufferView(candidate)) {
    const byteLength = isArrayBufferView(candidate)
      ? arrayBufferViewByteLength(candidate as ArrayBufferView)
      : ArrayBufferPrototypeGetByteLength(candidate as ArrayBuffer);
    if (byteLength > OPENSSL_BIGNUM_MAX_BYTES) {
      throw new NodeError(
        "ERR_OSSL_BN_BIGNUM_TOO_LONG",
        "bignum too long",
      );
    }
    candidateBytes = candidate;
  } else {
    throw new ERR_INVALID_ARG_TYPE(
      "candidate",
      [
        "ArrayBuffer",
        "TypedArray",
        "Buffer",
        "DataView",
        "bigint",
      ],
      candidate,
    );
  }

  return op_node_check_prime_bytes(candidateBytes, checks);
}

function generatePrime(
  size: number,
  options: any = { __proto__: null },
  callback?: (err: Error | null, prime: ArrayBuffer | bigint) => void,
) {
  validateInt32(size, "size", 1);
  if (typeof options === "function") {
    callback = options;
    options = {};
  }
  validateFunction(callback, "callback");
  const {
    bigint,
    safe,
    add,
    rem,
  } = validateRandomPrimeJob(size, options);
  PromisePrototypeThen(
    op_node_gen_prime_async(size, safe, add ?? null, rem ?? null),
    (prime: Uint8Array) => {
      const buffer = TypedArrayPrototypeGetBuffer(prime);
      const result = bigint ? arrayBufferToUnsignedBigInt(buffer) : buffer;
      callback?.(null, result);
    },
    (err: Error) => {
      callback?.(err, null as unknown as ArrayBuffer);
    },
  );
}

function generatePrimeSync(
  size: number,
  options: any = { __proto__: null },
): ArrayBuffer | bigint {
  const {
    bigint,
    safe,
    add,
    rem,
  } = validateRandomPrimeJob(size, options);

  const prime = op_node_gen_prime(size, safe, add ?? null, rem ?? null);
  const buffer = TypedArrayPrototypeGetBuffer(prime);
  if (bigint) return arrayBufferToUnsignedBigInt(buffer);
  return buffer;
}

// Returns the byteLength of an ArrayBufferView using the correct primordial
// getter depending on whether it is a TypedArray or a DataView.
function arrayBufferViewByteLength(view: ArrayBufferView): number {
  return isTypedArray(view)
    ? TypedArrayPrototypeGetByteLength(view as Uint8Array)
    : DataViewPrototypeGetByteLength(view as DataView);
}

function toUint8Array(
  value: ArrayBuffer | ArrayBufferView | Buffer,
): Uint8Array {
  if (ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, value)) {
    return value as Uint8Array;
  }
  if (isArrayBufferView(value)) {
    const buffer = isTypedArray(value)
      ? TypedArrayPrototypeGetBuffer(value as Uint8Array)
      : DataViewPrototypeGetBuffer(value as DataView);
    const byteOffset = isTypedArray(value)
      ? TypedArrayPrototypeGetByteOffset(value as Uint8Array)
      : DataViewPrototypeGetByteOffset(value as DataView);
    return new Uint8Array(
      buffer,
      byteOffset,
      arrayBufferViewByteLength(value),
    );
  }
  return new Uint8Array(value);
}

function validateRandomPrimeJob(
  size: number,
  options: any,
): any {
  validateInt32(size, "size", 1);
  validateObject(options, "options");

  let {
    safe = false,
    bigint = false,
    add,
    rem,
  } = options!;

  validateBoolean(safe, "options.safe");
  validateBoolean(bigint, "options.bigint");

  if (add !== undefined) {
    if (typeof add === "bigint") {
      add = unsignedBigIntToBuffer(add, "options.add");
    } else if (!isAnyArrayBuffer(add) && !isArrayBufferView(add)) {
      throw new ERR_INVALID_ARG_TYPE(
        "options.add",
        [
          "ArrayBuffer",
          "TypedArray",
          "Buffer",
          "DataView",
          "bigint",
        ],
        add,
      );
    }
  }

  if (rem !== undefined) {
    if (typeof rem === "bigint") {
      rem = unsignedBigIntToBuffer(rem, "options.rem");
    } else if (!isAnyArrayBuffer(rem) && !isArrayBufferView(rem)) {
      throw new ERR_INVALID_ARG_TYPE(
        "options.rem",
        [
          "ArrayBuffer",
          "TypedArray",
          "Buffer",
          "DataView",
          "bigint",
        ],
        rem,
      );
    }
  }

  const addBuf = add ? toUint8Array(add) : undefined;
  const remBuf = rem ? toUint8Array(rem) : undefined;

  if (addBuf) {
    const addBitCount = bitCount(addBuf);
    if (addBitCount === 0) {
      throw new NodeRangeError("ERR_OUT_OF_RANGE", "invalid options.add");
    }
    if (addBitCount > size) {
      throw new NodeRangeError("ERR_OUT_OF_RANGE", "invalid options.add");
    }

    if (remBuf) {
      const addBigInt = bufferToBigInt(addBuf);
      const remBigInt = bufferToBigInt(remBuf);
      if (addBigInt <= remBigInt) {
        throw new NodeRangeError("ERR_OUT_OF_RANGE", "invalid options.rem");
      }
    }
  }

  return {
    safe,
    bigint,
    add: addBuf,
    rem: remBuf,
  };
}

function bigintToBytes(n: bigint): Uint8Array {
  if (n === 0n) return new Uint8Array([0]);
  const hex = BigIntPrototypeToString(n, 16);
  const padded = hex.length % 2 ? "0" + hex : hex;
  const bytes = new Uint8Array(padded.length / 2);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = NumberParseInt(
      StringPrototypeSubstring(padded, i * 2, i * 2 + 2),
      16,
    );
  }
  return bytes;
}

function bufferToBigInt(buf: Uint8Array): bigint {
  let result = 0n;
  for (let i = 0; i < buf.length; i++) {
    result = (result << 8n) | BigInt(buf[i]);
  }
  return result;
}

function bitCount(buf: Uint8Array): number {
  for (let i = 0; i < buf.length; i++) {
    if (buf[i] !== 0) {
      return (buf.length - i) * 8 - MathClz32(buf[i]) + 24;
    }
  }
  return 0;
}

const numberToHexCharCode = (number: number): number =>
  (number < 10 ? 48 : 87) + number;

function arrayBufferToUnsignedBigInt(buf: ArrayBuffer): bigint {
  const length = ArrayBufferPrototypeGetByteLength(buf);
  const chars: number[] = new Array(length * 2);
  const view = new DataView(buf);

  for (let i = 0; i < length; i++) {
    const val = DataViewPrototypeGetUint8(view, i);
    chars[2 * i] = numberToHexCharCode(val >> 4);
    chars[2 * i + 1] = numberToHexCharCode(val & 0xf);
  }

  return BigInt(`0x${StringFromCharCode(...new SafeArrayIterator(chars))}`);
}

function unsignedBigIntToBuffer(bigint: bigint, name: string) {
  if (bigint < 0) {
    throw new ERR_OUT_OF_RANGE(name, ">= 0", bigint);
  }

  const hex = BigIntPrototypeToString(bigint, 16);
  const padded = StringPrototypePadStart(
    hex,
    hex.length + (hex.length % 2),
    "0",
  );
  return Buffer.from(padded, "hex");
}

function randomUUID(options) {
  if (options !== undefined) {
    validateObject(options, "options");
  }
  const {
    disableEntropyCache = false,
  } = options || {};

  validateBoolean(disableEntropyCache, "options.disableEntropyCache");

  if (disableEntropyCache) {
    return randomUUIDUncached();
  }

  return globalThis.crypto.randomUUID();
}

function randomUUIDUncached() {
  const bytes = new Uint8Array(16);
  globalThis.crypto.getRandomValues(bytes);
  bytes[6] = (bytes[6] & 0x0f) | 0x40;
  bytes[8] = (bytes[8] & 0x3f) | 0x80;

  const chars: number[] = new Array(36);
  let charIndex = 0;
  for (let byteIndex = 0; byteIndex < 16; byteIndex++) {
    if (
      byteIndex === 4 || byteIndex === 6 || byteIndex === 8 ||
      byteIndex === 10
    ) {
      chars[charIndex++] = 45;
    }
    const byte = bytes[byteIndex];
    chars[charIndex++] = numberToHexCharCode(byte >> 4);
    chars[charIndex++] = numberToHexCharCode(byte & 0x0f);
  }

  return StringFromCharCode(...new SafeArrayIterator(chars));
}

return {
  checkPrime,
  checkPrimeSync,
  generatePrime,
  generatePrimeSync,
  randomUUID,
  randomInt,
  randomBytes,
  randomFill,
  randomFillSync,
  default: {
    checkPrime,
    checkPrimeSync,
    generatePrime,
    generatePrimeSync,
    randomUUID,
    randomInt,
    randomBytes,
    randomFill,
    randomFillSync,
  },
};
})();
