/**
 * Encoding Utilities
 * @countermeasure/web-components
 *
 * Base64 and hex encoding/decoding utilities for encryption operations.
 * Compatible with FluxEncrypt's encoding format.
 */

import { CryptoError } from './types'

// ============================================================================
// BASE64 ENCODING
// ============================================================================

/**
 * Encode a Uint8Array to base64 string.
 * Uses URL-safe base64 without padding for compatibility.
 */
export function toBase64(data: Uint8Array): string {
  if (typeof btoa === 'function') {
    const binary = String.fromCharCode(...data)
    return btoa(binary)
  }
  // Node.js fallback
  if (typeof Buffer !== 'undefined') {
    return Buffer.from(data).toString('base64')
  }
  throw new CryptoError('No base64 encoding available', 'ENCODING_ERROR')
}

/**
 * Decode a base64 string to Uint8Array.
 */
export function fromBase64(base64: string): Uint8Array {
  try {
    if (typeof atob === 'function') {
      const binary = atob(base64)
      const bytes = new Uint8Array(binary.length)
      for (let i = 0; i < binary.length; i++) {
        bytes[i] = binary.charCodeAt(i)
      }
      return bytes
    }
    // Node.js fallback
    if (typeof Buffer !== 'undefined') {
      return new Uint8Array(Buffer.from(base64, 'base64'))
    }
    throw new CryptoError('No base64 decoding available', 'ENCODING_ERROR')
  } catch (error) {
    if (error instanceof CryptoError) {
      throw error
    }
    throw new CryptoError('Invalid base64 string', 'ENCODING_ERROR', error)
  }
}

// ============================================================================
// HEX ENCODING
// ============================================================================

/**
 * Encode a Uint8Array to hex string.
 */
export function toHex(data: Uint8Array): string {
  return Array.from(data)
    .map(byte => byte.toString(16).padStart(2, '0'))
    .join('')
}

/**
 * Decode a hex string to Uint8Array.
 */
export function fromHex(hex: string): Uint8Array {
  // Remove any whitespace and validate
  const cleanHex = hex.replace(/\s/g, '').toLowerCase()

  if (cleanHex.length % 2 !== 0) {
    throw new CryptoError('Invalid hex string: odd length', 'ENCODING_ERROR')
  }

  if (!/^[0-9a-f]*$/.test(cleanHex)) {
    throw new CryptoError('Invalid hex string: non-hex characters', 'ENCODING_ERROR')
  }

  const bytes = new Uint8Array(cleanHex.length / 2)
  for (let i = 0; i < cleanHex.length; i += 2) {
    bytes[i / 2] = parseInt(cleanHex.slice(i, i + 2), 16)
  }
  return bytes
}

// ============================================================================
// STRING ENCODING
// ============================================================================

/**
 * Encode a string to Uint8Array using UTF-8.
 */
export function stringToBytes(str: string): Uint8Array {
  return new TextEncoder().encode(str)
}

/**
 * Decode a Uint8Array to string using UTF-8.
 */
export function bytesToString(bytes: Uint8Array): string {
  return new TextDecoder().decode(bytes)
}

// ============================================================================
// COMBINED FORMAT ENCODING
// ============================================================================

/**
 * Encode data with format prefix for self-describing ciphertext.
 * Format: [1 byte version][12 bytes IV][16 bytes tag][N bytes ciphertext]
 */
export function encodeEncryptedData(iv: Uint8Array, ciphertext: Uint8Array): Uint8Array {
  // Version 1 format: version(1) + iv(12) + ciphertext(includes tag)
  const VERSION = 0x01
  const result = new Uint8Array(1 + iv.length + ciphertext.length)
  result[0] = VERSION
  result.set(iv, 1)
  result.set(ciphertext, 1 + iv.length)
  return result
}

/**
 * Decode encrypted data from combined format.
 * Returns { iv, ciphertext } or throws on invalid format.
 */
export function decodeEncryptedData(data: Uint8Array): { iv: Uint8Array; ciphertext: Uint8Array } {
  if (data.length < 14) {
    // Minimum: 1 version + 12 IV + 1 ciphertext byte
    throw new CryptoError('Invalid encrypted data: too short', 'INVALID_CIPHERTEXT')
  }

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

  const iv = data.slice(1, 13)
  const ciphertext = data.slice(13)

  return { iv, ciphertext }
}

// ============================================================================
// UTILITIES
// ============================================================================

/**
 * Compare two Uint8Arrays in constant time to prevent timing attacks.
 */
export function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) {
    return false
  }

  let result = 0
  for (let i = 0; i < a.length; i++) {
    result |= (a[i] ?? 0) ^ (b[i] ?? 0)
  }
  return result === 0
}

/**
 * Securely clear a Uint8Array by overwriting with zeros.
 * Note: JavaScript doesn't guarantee memory clearing, but this is best effort.
 */
export function zeroize(data: Uint8Array): void {
  data.fill(0)
}

/**
 * Concatenate multiple Uint8Arrays.
 */
export function concat(...arrays: Uint8Array[]): Uint8Array {
  const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0)
  const result = new Uint8Array(totalLength)
  let offset = 0
  for (const arr of arrays) {
    result.set(arr, offset)
    offset += arr.length
  }
  return result
}
