const ENCRYPTION_VERSION = 1;
const PBKDF2_ITERATIONS = 250_000;
const KEY_BITS = 256;
const SALT_BYTES = 16;
const IV_BYTES = 12;

const encoder = new TextEncoder();
const decoder = new TextDecoder();

/**
 * @typedef {Object} EncryptedSecret
 * @property {1} version
 * @property {"PBKDF2-SHA256"} kdf
 * @property {number} iterations
 * @property {"AES-GCM"} cipher
 * @property {string} salt
 * @property {string} iv
 * @property {string} ciphertext
 */

/**
 * @param {Uint8Array} bytes
 */
function toArrayBuffer(bytes) {
  const buffer = new ArrayBuffer(bytes.byteLength);

  new Uint8Array(buffer).set(bytes);

  return buffer;
}

/**
 * @param {Uint8Array} bytes
 */
function bytesToBase64(bytes) {
  let binary = "";

  for (const byte of bytes) {
    binary += String.fromCharCode(byte);
  }

  return btoa(binary);
}

/**
 * @param {string} base64
 */
function base64ToBytes(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);

  for (let i = 0; i < binary.length; i += 1) {
    bytes[i] = binary.charCodeAt(i);
  }

  return bytes;
}

/**
 * @param {number} length
 */
function randomBytes(length) {
  const bytes = new Uint8Array(length);

  crypto.getRandomValues(bytes);

  return bytes;
}

/**
 * @param {string} password
 */
async function importPassword(password) {
  return crypto.subtle.importKey(
    "raw",
    encoder.encode(password),
    "PBKDF2",
    false,
    ["deriveKey"],
  );
}

/**
 * @param {string} password
 * @param {Uint8Array} salt
 * @param {number} iterations
 */
async function deriveKey(password, salt, iterations) {
  const key = await importPassword(password);

  return crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      hash: "SHA-256",
      salt: toArrayBuffer(salt),
      iterations,
    },
    key,
    {
      name: "AES-GCM",
      length: KEY_BITS,
    },
    false,
    ["encrypt", "decrypt"],
  );
}

/**
 * @param {string} secret
 * @param {string} password
 * @returns {Promise<EncryptedSecret>}
 */
async function encrypt(secret, password) {
  const salt = randomBytes(SALT_BYTES);
  const iv = randomBytes(IV_BYTES);
  const key = await deriveKey(password, salt, PBKDF2_ITERATIONS);
  const encrypted = await crypto.subtle.encrypt(
    {
      name: "AES-GCM",
      iv: toArrayBuffer(iv),
    },
    key,
    encoder.encode(secret),
  );

  return {
    version: ENCRYPTION_VERSION,
    kdf: "PBKDF2-SHA256",
    iterations: PBKDF2_ITERATIONS,
    cipher: "AES-GCM",
    salt: bytesToBase64(salt),
    iv: bytesToBase64(iv),
    ciphertext: bytesToBase64(new Uint8Array(encrypted)),
  };
}

/**
 * @param {EncryptedSecret} encrypted
 * @param {string} password
 */
async function decrypt(encrypted, password) {
  if (encrypted.version !== ENCRYPTION_VERSION) {
    throw new Error("Unsupported wallet encryption version");
  }

  const salt = base64ToBytes(encrypted.salt);
  const iv = base64ToBytes(encrypted.iv);
  const ciphertext = base64ToBytes(encrypted.ciphertext);
  const key = await deriveKey(password, salt, encrypted.iterations);
  const decrypted = await crypto.subtle.decrypt(
    {
      name: "AES-GCM",
      iv: toArrayBuffer(iv),
    },
    key,
    toArrayBuffer(ciphertext),
  );

  return decoder.decode(decrypted);
}

export const encryption = /** @type {const} */ ({
  encrypt,
  decrypt,
});
