///|
/// OpenPGP AEAD framing (draft-ietf-openpgp-rfc4880bis §5.16 / the
/// crypto-refresh §5.16 SEIPD-v2 chunk layer): an AEAD payload is split
/// into chunks of 2^(c+6) octets, each sealed with a per-chunk nonce and
/// associated data, followed by a final tag over the total plaintext
/// length. GnuPG 2.4.x produces exactly this shape for tag-20 packets.
///
/// Per-chunk AD:  0xD4 | version | cipher | aead | chunk-octet | u64(index)
/// Final AD:      0xD4 | version | cipher | aead | chunk-octet |
///                u64(index of the final chunk) | u64(total plaintext octets)
/// Nonce(n):      the packet IV with the u64 chunk index XORed into its
///                last 8 octets.

///|
/// AEAD algorithm IDs (RFC 9580 §9.6).
pub(all) enum AeadMode {
  Eax
  Ocb
  Gcm
} derive(Eq, Debug)

///|
pub extend AeadMode with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend AeadMode with Eq::{not_equal, equal}

///|
pub fn AeadMode::number(self : AeadMode) -> Int32 {
  match self {
    Eax => 1
    Ocb => 2
    Gcm => 3
  }
}

///|
pub fn aead_mode_of_number(n : Int32) -> AeadMode? {
  match n {
    1 => Some(Eax)
    2 => Some(Ocb)
    3 => Some(Gcm)
    _ => None
  }
}

///|
/// IV/nonce length of the mode (RFC 9580 §9.6).
pub fn AeadMode::iv_length(self : AeadMode) -> Int32 {
  match self {
    Eax => 16
    Ocb => 15
    Gcm => 12
  }
}

///|
/// Tag length — 16 octets for every registered mode.
pub fn AeadMode::tag_length(_self : AeadMode) -> Int32 {
  16
}

///|
/// Chunk size for the chunk-size octet: 2^(c + 6) octets. Computed as an
/// Int64 (Int is 32 bits here and the octet can encode up to 2^56);
/// callers clamp it against the remaining payload.
pub fn aead_chunk_size(chunk_octet : Int32) -> Int64 raise {
  guard chunk_octet >= 0 && chunk_octet <= 56 else {
    @error.fail("aead: chunk size octet \{chunk_octet} out of range")
  }
  let shift = chunk_octet + 6
  let mut size : Int64 = 1L
  let mut i = 0
  while i < shift {
    size = size * 2L
    i = i + 1
  }
  size
}

///|
/// Seal one chunk with `mode` under the chunk nonce (IV with the index
/// XORed into the last 8 octets).
fn aead_seal_chunk(
  bc : &@primitives.BlockCipher,
  mode : AeadMode,
  iv : BytesView,
  index : Int64,
  ad : BytesView,
  plain : BytesView,
) -> Bytes raise {
  let nonce = aead_chunk_nonce(iv, index)
  match mode {
    Ocb => ocb_encrypt(bc, nonce.exact_view(), ad, plain)
    Gcm => gcm_encrypt(bc, nonce.exact_view(), ad, plain)
    Eax => eax_encrypt(bc, nonce.exact_view(), ad, plain)
  }
}

///|
/// Open one chunk, raising when the tag does not verify.
fn aead_open_chunk(
  bc : &@primitives.BlockCipher,
  mode : AeadMode,
  iv : BytesView,
  index : Int64,
  ad : BytesView,
  cipher : BytesView,
) -> Bytes raise {
  let nonce = aead_chunk_nonce(iv, index)
  match mode {
    Ocb => ocb_decrypt(bc, nonce.exact_view(), ad, cipher)
    Gcm => gcm_decrypt(bc, nonce.exact_view(), ad, cipher)
    Eax => eax_decrypt(bc, nonce.exact_view(), ad, cipher)
  }
}

///|
/// The per-chunk nonce: the packet IV with the 8-octet big-endian chunk
/// index XORed into its tail. The index is a parameter, NOT read from the
/// AD tail — the final tag's AD ends with the total-octet count.
fn aead_chunk_nonce(iv : BytesView, index : Int64) -> Bytes raise {
  guard iv.length() >= 8 else { @error.fail("aead: IV shorter than 8 octets") }
  let idx : Array[Byte] = Array(capacity=8)
  for i in 0..<8 {
    let shift = (7 - i) * 8
    idx.push(((index >> shift) & 0xFFL).to_int().to_byte())
  }
  let out : Array[Byte] = Array(capacity=iv.length())
  for i in 0..= iv.length() {
      idx[i + 8 - iv.length()].to_int()
    } else {
      0
    }
    out.push((iv[i].to_int() ^ x).to_byte())
  }
  Bytes::from_array(out)
}

///|
/// Assemble AD bytes: header5 || u64(index) || tail.
fn aead_ad(header5 : BytesView, index : Int64, tail : BytesView) -> Bytes {
  let out : Array[Byte] = Array(capacity=5 + 8 + tail.length())
  for b in header5 {
    out.push(b)
  }
  for i in 0..<8 {
    let shift = (7 - i) * 8
    out.push(((index >> shift) & 0xFFL).to_int().to_byte())
  }
  for b in tail {
    out.push(b)
  }
  Bytes::from_array(out)
}

///|
/// Seal a full AEAD payload (the OpenPGP chunk layer). `header5` is
/// 0xD4 | version | cipher | aead | chunk-octet; `iv` is the packet IV.
/// Returns the packet body tail: chunk ciphertexts+tags followed by the
/// final tag. An empty payload still produces one empty chunk tag plus
/// the final tag (what go-crypto/OpenPGP.js emit).
pub fn aead_seal(
  bc : &@primitives.BlockCipher,
  mode : AeadMode,
  header5 : BytesView,
  iv : BytesView,
  chunk_octet : Int32,
  plain : BytesView,
) -> Bytes raise {
  guard iv.length() == mode.iv_length() else {
    @error.fail("aead: bad IV length for the mode")
  }
  let chunk_size = aead_chunk_size(chunk_octet)
  let out = Buffer(size_hint=plain.length() + 32)
  let mut index : Int64 = 0L
  let mut off = 0
  // At least one chunk is emitted, even for an empty payload.
  while off < plain.length() || index == 0L {
    let remaining = plain.length() - off
    let want = if remaining.to_int64() < chunk_size {
      remaining
    } else {
      chunk_size.to_int()
    }
    let ad = aead_ad(header5, index, Bytes::new(0).exact_view())
    out.write_bytes(
      aead_seal_chunk(
        bc,
        mode,
        iv,
        index,
        ad.exact_view(),
        plain[off:off + want],
      ).exact_view(),
    )
    off = off + want
    index = index + 1L
  }
  // Final tag: empty plaintext, index = number of data chunks, and the
  // total plaintext octet count in the AD tail.
  let total : Array[Byte] = Array(capacity=8)
  let total_i64 : Int64 = plain.length().to_int64()
  for i in 0..<8 {
    let shift = (7 - i) * 8
    total.push(((total_i64 >> shift) & 0xFFL).to_int().to_byte())
  }
  let fad = aead_ad(header5, index, Bytes::from_array(total).exact_view())
  out.write_bytes(
    aead_seal_chunk(
      bc,
      mode,
      iv,
      index,
      fad.exact_view(),
      Bytes::new(0).exact_view(),
    ).exact_view(),
  )
  out.to_bytes()
}

///|
/// Open a full AEAD payload (the inverse of `aead_seal`). The final tag
/// must verify against the total plaintext length; any chunk tag failure
/// raises.
pub fn aead_open(
  bc : &@primitives.BlockCipher,
  mode : AeadMode,
  header5 : BytesView,
  iv : BytesView,
  chunk_octet : Int32,
  cipher : BytesView,
) -> Bytes raise {
  guard iv.length() == mode.iv_length() else {
    @error.fail("aead: bad IV length for the mode")
  }
  guard cipher.length() >= mode.tag_length() else {
    @error.fail("aead: payload shorter than the final tag")
  }
  let chunk_size = aead_chunk_size(chunk_octet)
  let out = Buffer(size_hint=cipher.length())
  let mut index : Int64 = 0L
  let mut pos = 0
  // Data chunks: while more than the final tag remains. Each chunk
  // reserves its own tag AND the final tag, so the last 16 octets are
  // always the final tag (a payload that is an exact chunk multiple has
  // no trailing empty chunk, but an all-empty payload does).
  while cipher.length() - pos > mode.tag_length() {
    let remaining = cipher.length() - pos
    let available = remaining - mode.tag_length() - mode.tag_length()
    let want = if available <= 0 {
      0
    } else if available.to_int64() < chunk_size {
      available
    } else {
      chunk_size.to_int()
    }
    let ad = aead_ad(header5, index, Bytes::new(0).exact_view())
    out.write_bytes(
      aead_open_chunk(
        bc,
        mode,
        iv,
        index,
        ad.exact_view(),
        cipher[pos:pos + want + mode.tag_length()],
      ).exact_view(),
    )
    pos = pos + want + mode.tag_length()
    index = index + 1L
  }
  // Final tag.
  let total : Array[Byte] = Array(capacity=8)
  let total_i64 : Int64 = out.length().to_int64()
  for i in 0..<8 {
    let shift = (7 - i) * 8
    total.push(((total_i64 >> shift) & 0xFFL).to_int().to_byte())
  }
  let fad = aead_ad(header5, index, Bytes::from_array(total).exact_view())
  ignore(
    aead_open_chunk(
      bc,
      mode,
      iv,
      index,
      fad.exact_view(),
      cipher[pos:cipher.length()],
    ),
  )
  out.to_bytes()
}