/**
 * @bytelyst/client-encrypt — AES-256-GCM via Web Crypto API
 *
 * Works in browsers (window.crypto.subtle) and React Native (expo-crypto polyfill).
 * Produces EncryptedField objects wire-compatible with:
 * - @bytelyst/field-encrypt (Node.js server)
 * - BLFieldEncrypt (Swift CryptoKit / Kotlin javax.crypto)
 */

import type { EncryptedField } from './types.js';
import { toHex, fromHex } from './hex.js';

const ALGORITHM = 'AES-GCM';
const KEY_SIZE_BITS = 256;
const IV_BYTES = 12;
const TAG_BITS = 128;

/** Get the SubtleCrypto instance (browser or globalThis). */
function getSubtle(): SubtleCrypto {
  if (typeof globalThis !== 'undefined' && globalThis.crypto?.subtle) {
    return globalThis.crypto.subtle;
  }
  throw new Error(
    '@bytelyst/client-encrypt requires Web Crypto API (SubtleCrypto). ' +
      'Use a polyfill in React Native (e.g., expo-crypto).'
  );
}

/** Get the crypto object for random bytes. */
function getCrypto(): Crypto {
  if (typeof globalThis !== 'undefined' && globalThis.crypto) {
    return globalThis.crypto;
  }
  throw new Error('@bytelyst/client-encrypt requires globalThis.crypto for random bytes.');
}

/**
 * Encrypt a plaintext string with AES-256-GCM using Web Crypto API.
 *
 * @param plaintext - UTF-8 string to encrypt
 * @param key       - CryptoKey (AES-GCM, 256-bit)
 * @param dekId     - DEK identifier stored in the output
 * @param aad       - Optional additional authenticated data
 * @returns EncryptedField with hex-encoded ciphertext, IV, and tag
 */
export async function encryptField(
  plaintext: string,
  key: CryptoKey,
  dekId: string,
  aad?: string
): Promise<EncryptedField> {
  const subtle = getSubtle();
  const crypto = getCrypto();

  const iv = new Uint8Array(IV_BYTES);
  crypto.getRandomValues(iv);

  const encoder = new TextEncoder();
  const plaintextBytes = encoder.encode(plaintext);

  const params: AesGcmParams = {
    name: ALGORITHM,
    iv: iv.buffer as ArrayBuffer,
    tagLength: TAG_BITS,
  };

  if (aad) {
    params.additionalData = encoder.encode(aad).buffer as ArrayBuffer;
  }

  // Web Crypto returns ciphertext || tag concatenated
  const ciphertextWithTag = new Uint8Array(
    await subtle.encrypt(params, key, plaintextBytes.buffer as ArrayBuffer)
  );

  const tagOffset = ciphertextWithTag.length - TAG_BITS / 8;
  const ct = ciphertextWithTag.slice(0, tagOffset);
  const tag = ciphertextWithTag.slice(tagOffset);

  return {
    __encrypted: true,
    v: 1,
    alg: 'aes-256-gcm',
    ct: toHex(ct),
    iv: toHex(iv),
    tag: toHex(tag),
    dekId,
  };
}

/**
 * Decrypt an EncryptedField back to plaintext.
 *
 * @param field - EncryptedField object
 * @param key   - CryptoKey (must match the key used to encrypt)
 * @param aad   - Optional AAD (must match the AAD used during encryption)
 * @returns Decrypted UTF-8 string
 * @throws DOMException if authentication tag verification fails
 */
export async function decryptField(
  field: EncryptedField,
  key: CryptoKey,
  aad?: string
): Promise<string> {
  const subtle = getSubtle();

  const iv = fromHex(field.iv);
  const ct = fromHex(field.ct);
  const tag = fromHex(field.tag);

  // Web Crypto expects ciphertext || tag concatenated
  const ciphertextWithTag = new Uint8Array(ct.length + tag.length);
  ciphertextWithTag.set(ct, 0);
  ciphertextWithTag.set(tag, ct.length);

  const params: AesGcmParams = {
    name: ALGORITHM,
    iv: iv.buffer as ArrayBuffer,
    tagLength: TAG_BITS,
  };

  if (aad) {
    params.additionalData = new TextEncoder().encode(aad).buffer as ArrayBuffer;
  }

  const plaintextBytes = new Uint8Array(
    await subtle.decrypt(params, key, ciphertextWithTag.buffer as ArrayBuffer)
  );

  return new TextDecoder().decode(plaintextBytes);
}

/**
 * Generate a random AES-256-GCM CryptoKey.
 *
 * @param extractable - Whether the key material can be exported (default: true).
 *   Set to `false` for non-extractable keys stored in IndexedDB.
 */
export async function generateKey(extractable = true): Promise<CryptoKey> {
  const subtle = getSubtle();
  return subtle.generateKey({ name: ALGORITHM, length: KEY_SIZE_BITS }, extractable, [
    'encrypt',
    'decrypt',
  ]);
}

/**
 * Import a hex-encoded key string as a CryptoKey.
 *
 * @param hex         - 64 hex chars = 32 bytes
 * @param extractable - Whether the imported key can be exported (default: true)
 */
export async function keyFromHex(hex: string, extractable = true): Promise<CryptoKey> {
  const subtle = getSubtle();
  const keyBytes = fromHex(hex);
  if (keyBytes.length !== KEY_SIZE_BITS / 8) {
    throw new Error(`AES-256-GCM requires a 32-byte key, got ${keyBytes.length}`);
  }
  return subtle.importKey(
    'raw',
    keyBytes.buffer as ArrayBuffer,
    { name: ALGORITHM, length: KEY_SIZE_BITS },
    extractable,
    ['encrypt', 'decrypt']
  );
}

/**
 * Export a CryptoKey to a hex-encoded string.
 * Only works if the key was created with `extractable: true`.
 */
export async function keyToHex(key: CryptoKey): Promise<string> {
  const subtle = getSubtle();
  const raw = new Uint8Array(await subtle.exportKey('raw', key));
  return toHex(raw);
}

/**
 * Derive an AES-256 key from a passphrase using PBKDF2.
 *
 * @param passphrase  - User passphrase
 * @param salt        - Random salt (at least 16 bytes recommended)
 * @param iterations  - PBKDF2 iterations (default: 600,000 per OWASP 2023)
 * @param extractable - Whether derived key can be exported (default: false)
 */
export async function deriveKey(
  passphrase: string,
  salt: Uint8Array,
  iterations = 600_000,
  extractable = false
): Promise<CryptoKey> {
  const subtle = getSubtle();
  const encoder = new TextEncoder();

  const baseKey = await subtle.importKey(
    'raw',
    encoder.encode(passphrase).buffer as ArrayBuffer,
    'PBKDF2',
    false,
    ['deriveKey']
  );

  return subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt: salt.buffer as ArrayBuffer,
      iterations,
      hash: 'SHA-256',
    },
    baseKey,
    { name: ALGORITHM, length: KEY_SIZE_BITS },
    extractable,
    ['encrypt', 'decrypt']
  );
}
