///|
/// Discord voice transport encryption modes supported by this package.
pub(all) enum EncryptionMode {
  AeadAes256GcmRtpsize
  AeadXChaCha20Poly1305Rtpsize
} derive(Debug, Eq)

///|
/// Errors raised by transport-mode negotiation and packet encryption.
pub(all) suberror VoiceCryptoError {
  NoCompatibleMode(modes~ : Array[String])
  ShimUnavailable(reason~ : String)
  InvalidSecretKey(length~ : Int)
  InvalidEncryptedPacket(reason~ : String)
  AuthenticationFailed(reason~ : String)
  AeadFailed(status~ : Int, reason~ : String)
} derive(Debug, Eq)

///|
/// Select the preferred common Discord transport encryption mode.
pub fn EncryptionMode::negotiate(
  modes : Array[String],
) -> EncryptionMode raise VoiceCryptoError {
  if modes.contains("aead_aes256_gcm_rtpsize") {
    AeadAes256GcmRtpsize
  } else if modes.contains("aead_xchacha20_poly1305_rtpsize") {
    AeadXChaCha20Poly1305Rtpsize
  } else {
    raise NoCompatibleMode(modes~)
  }
}

///|
fn EncryptionMode::code(self : EncryptionMode) -> Int {
  match self {
    AeadAes256GcmRtpsize => 0
    AeadXChaCha20Poly1305Rtpsize => 1
  }
}

///|
fn EncryptionMode::nonce_length(self : EncryptionMode) -> Int {
  match self {
    AeadAes256GcmRtpsize => 12
    AeadXChaCha20Poly1305Rtpsize => 24
  }
}

///|
fn raise_voice_shim_status(status : Int) -> Unit raise VoiceCryptoError {
  let reason = voice_shim_error_message()
  match status {
    -100 | -101 | -102 => raise ShimUnavailable(reason~)
    -3 => raise AuthenticationFailed(reason~)
    _ => raise AeadFailed(status~, reason~)
  }
}

///|
fn aead_seal_raw(
  mode : EncryptionMode,
  key : Bytes,
  nonce : Bytes,
  aad : Bytes,
  plaintext : Bytes,
) -> Bytes raise VoiceCryptoError {
  let result = voice_shim_aead_seal(mode.code(), key, nonce, aad, plaintext)
  let status = voice_shim_last_status()
  if status != 0 {
    raise_voice_shim_status(status)
  }
  result
}

///|
fn aead_open_raw(
  mode : EncryptionMode,
  key : Bytes,
  nonce : Bytes,
  aad : Bytes,
  ciphertext : Bytes,
) -> Bytes raise VoiceCryptoError {
  let result = voice_shim_aead_open(mode.code(), key, nonce, aad, ciphertext)
  let status = voice_shim_last_status()
  if status != 0 {
    raise_voice_shim_status(status)
  }
  result
}

///|
fn nonce_suffix(counter : UInt) -> Bytes {
  Bytes::from_array([
    (counter >> 24).to_byte(),
    (counter >> 16).to_byte(),
    (counter >> 8).to_byte(),
    counter.to_byte(),
  ])
}

///|
fn make_nonce(mode : EncryptionMode, suffix : Bytes) -> Bytes {
  let bytes : Array[Byte] = []
  for byte in suffix {
    bytes.push(byte)
  }
  for _ in suffix.length().. TransportCipher raise VoiceCryptoError {
  if secret_key.length() != 32 {
    raise InvalidSecretKey(length=secret_key.length())
  }
  if !shim_available() {
    raise ShimUnavailable(
      reason=shim_unavailable_reason().unwrap_or("voice shim unavailable"),
    )
  }
  { mode, secret_key, counter: 0, }
}

///|
/// Encrypt an RTP payload and append the four-byte nonce suffix.
pub fn TransportCipher::seal(
  self : TransportCipher,
  header~ : Bytes,
  plaintext : Bytes,
) -> Bytes raise VoiceCryptoError {
  let suffix = nonce_suffix(self.counter)
  let nonce = make_nonce(self.mode, suffix)
  let encrypted = aead_seal_raw(
    self.mode,
    self.secret_key,
    nonce,
    header,
    plaintext,
  )
  self.counter += 1
  header + encrypted + suffix
}

///|
/// Authenticate and decrypt a packet, returning `header || plaintext`.
pub fn TransportCipher::open(
  self : TransportCipher,
  packet : Bytes,
  header_len~ : Int,
) -> Bytes raise VoiceCryptoError {
  if header_len < 0 || header_len > packet.length() {
    raise InvalidEncryptedPacket(reason="invalid RTP header length")
  }
  if packet.length() < header_len + 20 {
    raise InvalidEncryptedPacket(
      reason="packet is shorter than authentication tag and nonce suffix",
    )
  }
  let suffix = packet[packet.length() - 4:].to_owned()
  let nonce = make_nonce(self.mode, suffix)
  let header = packet[:header_len].to_owned()
  let ciphertext = packet[header_len:packet.length() - 4].to_owned()
  let plaintext = aead_open_raw(
    self.mode,
    self.secret_key,
    nonce,
    header,
    ciphertext,
  )
  header + plaintext
}