///|
/// RFC 9580 §5.13.2 Version 2 Symmetrically Encrypted and Integrity
/// Protected Data packet: an AEAD payload split into chunks whose message
/// key and nonce are derived from the session key and a 32-octet salt.
///
/// This differs from the 4880bis tag-20 layer in `aead.mbt` in three ways:
///
/// * the packet tag octet is 0xD2 (tag 18, not tag 20);
/// * the per-chunk associated data is only the 5-octet header (no chunk
///   index), and the final tag's AD appends the total plaintext length;
/// * the nonce is `HKDF-derived IV || u64be(chunk index)` (concatenation,
///   not an XOR into the packet IV).

///|
/// Derive the message key and the fixed part of the AEAD nonce for a v2
/// SEIPD (RFC 9580 §5.13.2): HKDF-SHA256 with the session key as IKM, the
/// packet salt as salt, and `0xD2 | version | cipher | aead | chunk` (the
/// packet header, `header5`) as info. The leftmost `key_len` octets are the
/// message key; the remaining `mode.iv_length() - 8` are the nonce prefix,
/// whose low 64 bits carry the chunk index.
pub fn seipd_v2_kdf(
  session_key : BytesView,
  salt : BytesView,
  key_len : Int32,
  mode : AeadMode,
  header5 : BytesView,
) -> (Bytes, Bytes) raise {
  guard salt.length() == 32 else {
    @error.fail("seipd_v2: the salt must be 32 octets")
  }
  guard header5.length() == 5 else {
    @error.fail("seipd_v2: the header must be 5 octets")
  }
  let iv_len = mode.iv_length()
  guard iv_len >= 8 else { @error.fail("seipd_v2: AEAD nonce shorter than 8") }
  let want = key_len + iv_len - 8
  let okm = @primitives.hkdf_sha256(session_key, salt, header5, want)
  (okm[0:key_len].to_owned(), okm[key_len:want].to_owned())
}

///|
/// The v2 chunk nonce: `iv || u64be(index)`.
fn seipd_v2_nonce(iv : BytesView, index : Int64) -> Bytes raise {
  guard iv.length() + 8 <= 16 + 8 else {
    @error.fail("seipd_v2: bad nonce prefix length")
  }
  let out : Array[Byte] = Array(capacity=iv.length() + 8)
  for b in iv {
    out.push(b)
  }
  for i in 0..<8 {
    let shift = (7 - i) * 8
    out.push(((index >> shift) & 0xFFL).to_int().to_byte())
  }
  Bytes::from_array(out)
}

///|
/// `header5 || u64be(total)` — the final tag's associated data.
fn seipd_v2_final_ad(header5 : BytesView, total : Int64) -> Bytes {
  let out : Array[Byte] = Array(capacity=13)
  for b in header5 {
    out.push(b)
  }
  for i in 0..<8 {
    let shift = (7 - i) * 8
    out.push(((total >> shift) & 0xFFL).to_int().to_byte())
  }
  Bytes::from_array(out)
}

///|
/// Seal a v2 SEIPD payload. `header5` is
/// `0xD2 | 0x02 | cipher | aead | chunk-octet`; `iv` is the HKDF nonce
/// prefix (`mode.iv_length() - 8` octets). Returns the packet tail: chunk
/// ciphertexts+tags followed by the final tag.
pub fn seipd_v2_seal(
  bc : &@primitives.BlockCipher,
  mode : AeadMode,
  header5 : BytesView,
  iv : BytesView,
  chunk_octet : Int32,
  plain : BytesView,
) -> Bytes raise {
  guard header5.length() == 5 else {
    @error.fail("seipd_v2: the header must be 5 octets")
  }
  guard iv.length() == mode.iv_length() - 8 else {
    @error.fail("seipd_v2: bad nonce prefix length")
  }
  // RFC 9580 §5.13.2: implementations MUST NOT create a chunk octet above
  // 16 (4 MiB chunks), though they must accept 0..16 when reading.
  guard chunk_octet >= 0 && chunk_octet <= 16 else {
    @error.fail("seipd_v2: chunk size octet \{chunk_octet} above the 16 limit")
  }
  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
  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 nonce = seipd_v2_nonce(iv, index)
    let chunk_ct = match mode {
      Ocb => ocb_encrypt(bc, nonce.exact_view(), header5, plain[off:off + want])
      Gcm => gcm_encrypt(bc, nonce.exact_view(), header5, plain[off:off + want])
      Eax => eax_encrypt(bc, nonce.exact_view(), header5, plain[off:off + want])
    }
    out.write_bytes(chunk_ct.exact_view())
    off = off + want
    index = index + 1L
  }
  let final_ad = seipd_v2_final_ad(header5, plain.length().to_int64())
  let final_nonce = seipd_v2_nonce(iv, index)
  let tag = match mode {
    Ocb => ocb_encrypt(bc, final_nonce.exact_view(), final_ad.exact_view(), b"")
    Gcm => gcm_encrypt(bc, final_nonce.exact_view(), final_ad.exact_view(), b"")
    Eax => eax_encrypt(bc, final_nonce.exact_view(), final_ad.exact_view(), b"")
  }
  out.write_bytes(tag.exact_view())
  out.to_bytes()
}

///|
/// Open a v2 SEIPD payload (the inverse of `seipd_v2_seal`). Every chunk
/// tag and the final tag must verify.
pub fn seipd_v2_open(
  bc : &@primitives.BlockCipher,
  mode : AeadMode,
  header5 : BytesView,
  iv : BytesView,
  chunk_octet : Int32,
  cipher : BytesView,
) -> Bytes raise {
  guard header5.length() == 5 else {
    @error.fail("seipd_v2: the header must be 5 octets")
  }
  guard iv.length() == mode.iv_length() - 8 else {
    @error.fail("seipd_v2: bad nonce prefix length")
  }
  // RFC 9580 §5.13.2: values 0..16 MUST be accepted.
  guard chunk_octet >= 0 && chunk_octet <= 16 else {
    @error.fail("seipd_v2: chunk size octet \{chunk_octet} out of range")
  }
  guard cipher.length() >= mode.tag_length() else {
    @error.fail("seipd_v2: 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
  while cipher.length() - pos > mode.tag_length() {
    let remaining = cipher.length() - pos
    // Reserve this chunk's tag and the final tag.
    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 nonce = seipd_v2_nonce(iv, index)
    let opened = match mode {
      Ocb =>
        ocb_decrypt(
          bc,
          nonce.exact_view(),
          header5,
          cipher[pos:pos + want + mode.tag_length()],
        )
      Gcm =>
        gcm_decrypt(
          bc,
          nonce.exact_view(),
          header5,
          cipher[pos:pos + want + mode.tag_length()],
        )
      Eax =>
        eax_decrypt(
          bc,
          nonce.exact_view(),
          header5,
          cipher[pos:pos + want + mode.tag_length()],
        )
    }
    out.write_bytes(opened.exact_view())
    pos = pos + want + mode.tag_length()
    index = index + 1L
  }
  let total : Int64 = out.length().to_int64()
  let final_ad = seipd_v2_final_ad(header5, total)
  let final_nonce = seipd_v2_nonce(iv, index)
  let opened = match mode {
    Ocb =>
      ocb_decrypt(
        bc,
        final_nonce.exact_view(),
        final_ad.exact_view(),
        cipher[pos:cipher.length()],
      )
    Gcm =>
      gcm_decrypt(
        bc,
        final_nonce.exact_view(),
        final_ad.exact_view(),
        cipher[pos:cipher.length()],
      )
    Eax =>
      eax_decrypt(
        bc,
        final_nonce.exact_view(),
        final_ad.exact_view(),
        cipher[pos:cipher.length()],
      )
  }
  guard opened.length() == 0 else {
    @error.fail("seipd_v2: the final tag covers a non-empty plaintext")
  }
  out.to_bytes()
}