/**
 * Hybrid Encryption (RSA-OAEP + AES-GCM)
 * @countermeasure/web-components
 *
 * Hybrid encryption combining RSA-OAEP for key exchange and AES-256-GCM for data.
 * Compatible with FluxEncrypt's HybridCipher.
 */

import { stringToBytes } from './encoding'
import { encodeOutput, decodeInput } from './encoding-helpers'
import { generateIV } from './keys'
import { CryptoError, type HybridCipherConfig, type EncodingFormat } from './types'

// ============================================================================
// CONSTANTS
// ============================================================================

const AES_KEY_LENGTH = 256
const AES_GCM_TAG_LENGTH = 128 // bits
const HYBRID_VERSION = 0x02 // Version 2 for hybrid format

// ============================================================================
// HYBRID CIPHER CLASS
// ============================================================================

/**
 * Hybrid cipher combining RSA-OAEP and AES-256-GCM.
 * Mirrors FluxEncrypt's HybridCipher API.
 *
 * The encryption format is:
 * [1 byte version][2 bytes RSA ciphertext length][N bytes RSA ciphertext][12 bytes IV][M bytes AES ciphertext]
 *
 * Usage:
 * ```typescript
 * const keypair = await generateKeyPair()
 * const cipher = createHybridCipher()
 * const encrypted = await cipher.encrypt(keypair.publicKey, 'secret data')
 * const decrypted = await cipher.decrypt(keypair.privateKey, encrypted)
 * ```
 */
export class HybridCipherImpl {
  private encoding: EncodingFormat
  private rsaKeySize: 2048 | 4096

  constructor(config: HybridCipherConfig = {}) {
    this.encoding = config.encoding ?? 'base64'
    this.rsaKeySize = config.rsaKeySize ?? 4096
  }

  /**
   * Encrypt data using hybrid encryption.
   * 1. Generate a random AES key
   * 2. Encrypt the AES key with RSA-OAEP
   * 3. Encrypt the data with AES-GCM
   * 4. Combine: version + RSA ciphertext length + RSA ciphertext + IV + AES ciphertext
   */
  async encrypt(publicKey: CryptoKey, plaintext: string | Uint8Array): Promise<string> {
    try {
      const data = typeof plaintext === 'string' ? stringToBytes(plaintext) : plaintext

      // Generate random AES key
      const aesKey = await crypto.subtle.generateKey(
        { name: 'AES-GCM', length: AES_KEY_LENGTH },
        true,
        ['encrypt', 'decrypt']
      )

      // Export AES key for RSA encryption
      const rawAesKey = await crypto.subtle.exportKey('raw', aesKey)

      // Encrypt AES key with RSA-OAEP
      const encryptedAesKey = await crypto.subtle.encrypt(
        { name: 'RSA-OAEP' },
        publicKey,
        rawAesKey
      )

      // Generate IV and encrypt data with AES-GCM
      const iv = generateIV()
      const encryptedData = await crypto.subtle.encrypt(
        {
          name: 'AES-GCM',
          iv: iv as unknown as Uint8Array<ArrayBuffer>,
          tagLength: AES_GCM_TAG_LENGTH,
        },
        aesKey,
        data as unknown as BufferSource
      )

      // Combine all parts
      const rsaCiphertext = new Uint8Array(encryptedAesKey)
      const aesCiphertext = new Uint8Array(encryptedData)

      // Format: version(1) + rsaLen(2) + rsaCiphertext(N) + iv(12) + aesCiphertext(M)
      const rsaLen = rsaCiphertext.length
      const combined = new Uint8Array(1 + 2 + rsaLen + 12 + aesCiphertext.length)
      combined[0] = HYBRID_VERSION
      combined[1] = (rsaLen >> 8) & 0xff
      combined[2] = rsaLen & 0xff
      combined.set(rsaCiphertext, 3)
      combined.set(iv, 3 + rsaLen)
      combined.set(aesCiphertext, 3 + rsaLen + 12)

      return encodeOutput(combined, this.encoding)
    } catch (error) {
      if (error instanceof CryptoError) {
        throw error
      }
      throw new CryptoError('Hybrid encryption failed', 'ENCRYPTION_FAILED', error)
    }
  }

  /**
   * Decrypt data using hybrid encryption.
   * 1. Parse the combined ciphertext
   * 2. Decrypt the AES key with RSA-OAEP
   * 3. Decrypt the data with AES-GCM
   */
  async decrypt(privateKey: CryptoKey, ciphertext: string): Promise<Uint8Array> {
    try {
      const data = decodeInput(ciphertext, this.encoding)

      // Parse format
      if (data.length < 16) {
        throw new CryptoError('Invalid hybrid ciphertext: too short', 'INVALID_CIPHERTEXT')
      }

      const version = data[0]
      if (version !== HYBRID_VERSION) {
        throw new CryptoError(
          `Unsupported hybrid encryption version: ${String(version)}`,
          'INVALID_CIPHERTEXT'
        )
      }

      const rsaLen = ((data[1] ?? 0) << 8) | (data[2] ?? 0)
      if (data.length < 3 + rsaLen + 12 + 1) {
        throw new CryptoError('Invalid hybrid ciphertext: truncated', 'INVALID_CIPHERTEXT')
      }

      const rsaCiphertext = data.slice(3, 3 + rsaLen)
      const iv = data.slice(3 + rsaLen, 3 + rsaLen + 12)
      const aesCiphertext = data.slice(3 + rsaLen + 12)

      // Decrypt AES key with RSA-OAEP
      const rawAesKey = await crypto.subtle.decrypt(
        { name: 'RSA-OAEP' },
        privateKey,
        rsaCiphertext as unknown as BufferSource
      )

      // Import decrypted AES key
      const aesKey = await crypto.subtle.importKey(
        'raw',
        rawAesKey,
        { name: 'AES-GCM', length: AES_KEY_LENGTH },
        false,
        ['decrypt']
      )

      // Decrypt data with AES-GCM
      const plaintext = await crypto.subtle.decrypt(
        {
          name: 'AES-GCM',
          iv: iv as unknown as Uint8Array<ArrayBuffer>,
          tagLength: AES_GCM_TAG_LENGTH,
        },
        aesKey,
        aesCiphertext as unknown as BufferSource
      )

      return new Uint8Array(plaintext)
    } catch (error) {
      if (error instanceof CryptoError) {
        throw error
      }
      throw new CryptoError('Hybrid decryption failed', 'DECRYPTION_FAILED', error)
    }
  }

  /**
   * Decrypt and return as string.
   */
  async decryptToString(privateKey: CryptoKey, ciphertext: string): Promise<string> {
    const plaintext = await this.decrypt(privateKey, ciphertext)
    return new TextDecoder().decode(plaintext)
  }

  /**
   * Get the current encoding format.
   */
  getEncoding(): EncodingFormat {
    return this.encoding
  }

  /**
   * Get the RSA key size.
   */
  getRsaKeySize(): number {
    return this.rsaKeySize
  }
}

// ============================================================================
// FACTORY FUNCTIONS
// ============================================================================

/**
 * Create a hybrid cipher instance.
 */
export function createHybridCipher(config: HybridCipherConfig = {}): HybridCipherImpl {
  return new HybridCipherImpl(config)
}

// ============================================================================
// CONVENIENCE FUNCTIONS
// ============================================================================

/**
 * Encrypt data with a public key (one-shot operation).
 */
export async function hybridEncrypt(
  publicKey: CryptoKey,
  plaintext: string | Uint8Array,
  config: HybridCipherConfig = {}
): Promise<string> {
  const cipher = createHybridCipher(config)
  return cipher.encrypt(publicKey, plaintext)
}

/**
 * Decrypt data with a private key (one-shot operation).
 */
export async function hybridDecrypt(
  privateKey: CryptoKey,
  ciphertext: string,
  config: HybridCipherConfig = {}
): Promise<Uint8Array> {
  const cipher = createHybridCipher(config)
  return cipher.decrypt(privateKey, ciphertext)
}

/**
 * Decrypt data with a private key and return as string (one-shot operation).
 */
export async function hybridDecryptToString(
  privateKey: CryptoKey,
  ciphertext: string,
  config: HybridCipherConfig = {}
): Promise<string> {
  const cipher = createHybridCipher(config)
  return cipher.decryptToString(privateKey, ciphertext)
}
