///|
/// Build a PKESK for one recipient over the session key, choosing the
/// algorithm by the recipient's encryption-capable key packet.
fn create_pkesk(
  enc_key : @packet.PublicKeyPacket,
  session_key : BytesView,
  sym_algo : @primitives.CipherAlgo,
) -> @packet.PkeskPacket raise {
  match enc_key.algo() {
    @packet.X25519 => {
      // §5.1.6: native 32-octet ephemeral key, no algorithm octet and no
      // session-key checksum (the v2 SEIPD names the cipher). The X25519
      // PKESK shape is v6-only: it must precede a v2 SEIPD, so a v4
      // recipient with an X25519 key cannot be encrypted to here.
      guard enc_key.version() >= 6 else {
        @error.fail(
          "api: X25519 encryption requires a v6 key (v4 X25519 keys are import/verify only)",
        )
      }
      let (eph, wrapped) = x25519_wrap_session_key(enc_key, session_key)
      {
        version: 6,
        key_id: Bytes::new(0),
        key_version: enc_key.version(),
        fingerprint: enc_key.fingerprint(),
        pub_algo: @packet.X25519,
        encrypted: wrapped,
        ecdh_ephemeral: eph,
      }
    }
    @packet.Ecdh => {
      let (eph, wrapped) = ecdh_wrap_session_key(enc_key, session_key, sym_algo)
      {
        version: 3,
        key_id: enc_key.key_id(),
        key_version: 0,
        fingerprint: Bytes::new(0),
        pub_algo: @packet.Ecdh,
        encrypted: wrapped,
        ecdh_ephemeral: eph,
      }
    }
    a if a.is_rsa() => {
      let rsa = rsa_public_key(enc_key)
      // m = algo(1) || session key || 2-octet checksum, PKCS#1 v1.5.
      let payload = Bytes::from_array([sym_algo.number().to_byte()])
        .add(session_key.to_owned())
        .add(checksum_bytes(@packet.session_key_checksum(session_key)))
      let mpi = rsa.pkcs1_v15_encrypt(payload.exact_view())
      {
        version: 3,
        key_id: enc_key.key_id(),
        key_version: 0,
        fingerprint: Bytes::new(0),
        pub_algo: a,
        encrypted: mpi,
        ecdh_ephemeral: Bytes::new(0),
      }
    }
    _ => @error.fail("api: recipient key algorithm cannot encrypt")
  }
}

///|
/// Recover (symmetric algo id, session key) from a PKESK with the
/// matching unlocked secret key packet.
fn unwrap_pkesk(
  pkesk : @packet.PkeskPacket,
  sk : @packet.SecretKeyPacket,
) -> (Int32, Bytes) raise {
  match pkesk.pub_algo() {
    @packet.X25519 => x25519_unwrap_session_key(pkesk, sk)
    @packet.Ecdh => ecdh_unwrap_session_key(pkesk, sk)
    a if a.is_rsa() => rsa_unwrap_session_key(pkesk, sk)
    _ =>
      @error.fail(
        "api: unsupported PKESK algorithm \{pkesk.pub_algo().number()}",
      )
  }
}

///|
/// Encrypt plaintext to one or more recipients' public keys. Each
/// recipient gets its own fresh PKESK encoding of the session key
/// (RFC 9580 §5.1.3 requires re-encoding per key). Result: armored PGP
/// MESSAGE of PKESK(s) + SEIPD, v2 for v6 recipients and v1 otherwise.
pub fn encrypt_to_recipients(
  plaintext : BytesView,
  recipients : Array[PublicKey],
  file_name? : String,
  compress? : @compress.CompressedAlgo,
) -> String raise {
  let literal = @packet.LiteralPacket::binary(plaintext, file_name?)
  let literal_wire = @packet.serialize_packet(
    @packet.Literal,
    literal.to_body().exact_view(),
  )
  seal_to_recipients(
    maybe_compress_inner(literal_wire, compress?).exact_view(),
    recipients,
  )
}

///|
/// Seal a pre-serialized inner packet stream into PKESK(s) plus a SEIPD
/// container and armor the result. Shared by `encrypt_to_recipients` (inner
/// = the literal packet) and `sign_and_encrypt`'s
/// `encrypt_to_recipients` (inner = the literal packet) and
/// `sign_and_encrypt` (inner = OPS + literal + signature).
///
/// The container version must match the PKESK generation: "The payload
/// following any v6 PKESK or v6 SKESK packet MUST be a v2 SEIPD", and a v2
/// SEIPD must not be preceded by v3 PKESKs (RFC 9580 §5.1.1/§5.1.2, §10.3.2.1).
fn seal_to_recipients(
  inner : BytesView,
  recipients : Array[PublicKey],
) -> String raise {
  guard !recipients.is_empty() else { @error.fail("api: no recipients") }
  // A message cannot mix key generations: v6 PKESKs must precede a v2
  // SEIPD packet and v3 PKESKs a v1 one (RFC 9580 §5.1.1/§5.1.2).
  let v6 = recipients[0].primary.version() >= 6
  for recipient in recipients {
    guard (recipient.primary.version() >= 6) == v6 else {
      @error.fail("api: cannot mix v4 and v6 recipients in one message")
    }
  }
  // RFC 9580 §5.2.3.15/§13.2: every recipient must accept the chosen pair,
  // so take the first recipient's preference order and intersect it with
  // every other recipient's candidate list.
  let (sym_algo, aead_mode) = if v6 {
    pick_v6_ciphersuite(recipients)
  } else {
    (@primitives.Aes256, @cipher.Ocb)
  }
  let session_key = @primitives.random_bytes(sym_algo.key_size())
  let w = @packet.Writer::new()
  // Lifecycle gate (M1): refuse to spend a session key on a recipient
  // whose key is revoked or no longer encryption-capable. The clock is
  // real on native; on the non-native targets it reads 0, which disables
  // the time-dependent part of the checks (documented degradation).
  let now = @primitives.unix_now()
  for recipient in recipients {
    guard !recipient.is_revoked(now) else {
      @error.fail("api: recipient key is revoked")
    }
    guard recipient.can_encrypt(now) else {
      @error.fail("api: recipient key is expired or cannot encrypt")
    }
    let enc_key = match recipient.encryption_packet_at(now) {
      Some(k) => k
      None => @error.fail("api: recipient key cannot encrypt")
    }
    let pkesk = create_pkesk(enc_key, session_key.exact_view(), sym_algo)
    let body = pkesk.to_body()
    w.bytes(
      @packet.serialize_packet(@packet.Pkesk, body.exact_view()).exact_view(),
    )
  }
  let seipd : @packet.SeipdPacket = if v6 {
    seal_seipd_v2(sym_algo, aead_mode, SEIPD_V2_CHUNK_OCTET, session_key, inner)
  } else {
    // v1 SEIPD always uses the v4 profile's mandatory cipher. Built here,
    // not before the branch: a v6 ciphersuite may be AES-128/192, and an
    // eager AES-256 cipher would reject that session-key length.
    let bc = @primitives.new_block_cipher(
      @primitives.Aes256,
      session_key.exact_view(),
    )
    @packet.SeipdPacket::v1(@cipher.seipd_v1_encrypt(bc, inner).exact_view())
  }
  w.bytes(
    @packet.serialize_packet(@packet.Seipd, seipd.to_body().exact_view()).exact_view(),
  )
  @armor.Message.encode(w.to_bytes().exact_view())
}

///|
/// The chunk-size octet this library writes for v2 SEIPD packets:
/// 2^(14+6) = 1 MiB (RFC 9580 §5.13.2 forbids values above 16). Internal:
/// it is a writer-side policy knob, not part of the public API.
const SEIPD_V2_CHUNK_OCTET : Int32 = 14

///|
/// Seal a v2 SEIPD packet (RFC 9580 §5.13.2): fresh 32-octet salt, HKDF
/// message key and nonce, chunked AEAD.
fn seal_seipd_v2(
  algo : @primitives.CipherAlgo,
  mode : @cipher.AeadMode,
  chunk_octet : Int32,
  session_key : BytesView,
  plain : BytesView,
) -> @packet.SeipdPacket raise {
  guard session_key.length() == algo.key_size() else {
    @error.fail("api: session key length does not match the SEIPD v2 cipher")
  }
  let salt = @primitives.random_bytes(32)
  let header5 = Bytes::from_array([
    0xD2,
    2,
    algo.number().to_byte(),
    mode.number().to_byte(),
    chunk_octet.to_byte(),
  ])
  let (key, iv) = @cipher.seipd_v2_kdf(
    session_key,
    salt.exact_view(),
    algo.key_size(),
    mode,
    header5.exact_view(),
  )
  let bc = @primitives.new_block_cipher(algo, key.exact_view())
  let ct = @cipher.seipd_v2_seal(
    bc,
    mode,
    header5.exact_view(),
    iv.exact_view(),
    chunk_octet,
    plain,
  )
  {
    version: 2,
    cipher_algo: algo.number(),
    aead_algo: mode.number(),
    chunk_octet,
    salt,
    ciphertext: ct,
  }
}

///|
fn checksum_bytes(sum : Int32) -> Bytes {
  Bytes::from_array([(sum >> 8).to_byte(), sum.to_byte()])
}

///|
/// Decrypt a binary message with an unlocked private key. Matches PKESKs
/// against the key's ids (RSA or ECDH), recovers the session key, checks
/// the session-key checksum, and unwraps the SEIPD payload.
pub fn decrypt_binary_with_private_key(
  data : BytesView,
  key : PrivateKey,
  max_decompressed~ : Int32 = DEFAULT_MAX_DECOMPRESSED_BYTES,
) -> Bytes raise {
  let packets = @packet.parse_packets(data)
  let inner = match decrypt_inner_with_key(packets, key) {
    Some(b) => b
    None => @error.fail("api: message is not encrypted to this key")
  }
  extract_literal(inner, max_decompressed)
}

///|
/// Does this PKESK address the given (unlocked) secret key? v6 by
/// fingerprint, v3 by key id. Shared by the decrypt pipeline and the
/// session-key recovery flow.
fn pkesk_addresses(
  pkesk : @packet.PkeskPacket,
  sk : @packet.SecretKeyPacket,
) -> Bool {
  if pkesk.version() == 6 {
    // §5.1.2: a v6 PKESK identifies the recipient by fingerprint.
    let fp = sk.public.fingerprint() catch { _ => Bytes::new(0) }
    pkesk.fingerprint().length() > 0 && fp.equal(pkesk.fingerprint())
  } else {
    let id = sk.public.key_id() catch { _ => Bytes::new(0) }
    id.equal(pkesk.key_id())
  }
}

///|
/// True for a v3 PKESK whose key id is all zero — a hidden recipient
/// (S13): it matches no key by id, so every candidate secret key must be
/// tried, like go-crypto's KeyDecryption with a zero key id.
fn pkesk_is_anonymous(pkesk : @packet.PkeskPacket) -> Bool {
  if pkesk.version() == 6 {
    return false
  }
  let id = pkesk.key_id()
  let mut all_zero = id.length() == 8
  for b in id {
    if b != b'\x00' {
      all_zero = false
    }
  }
  all_zero
}

///|
/// Unwrap `pkesk` with `sk` and open the SEIPD container in `packets`.
/// Shared by both PKESK-consuming flows; raises on any failure.
fn decrypt_with_pkesk(
  packets : Array[@packet.RawPacket],
  pkesk : @packet.PkeskPacket,
  sk : @packet.SecretKeyPacket,
) -> Bytes raise {
  let (algo, session) = unwrap_pkesk(pkesk, sk)
  unwrap_seipd_inner(packets, algo, session, pkesk.version())
}

///|
/// Match a PKESK in `packets` against `key` — v6 by fingerprint, v3 by key
/// id, over the primary key and the encryption subkey — recover the session
/// key and return the raw inner packet stream of the SEIPD container. `None`
/// means no PKESK belongs to this key; a matched packet that cannot be
/// decrypted raises. A v3 PKESK with an all-zero key id (hidden recipient)
/// is tried against every candidate: a candidate that fails to unwrap is
/// simply not the recipient. Shared by `decrypt_binary_with_private_key` and
/// the sign/decrypt pipeline in `sign.mbt`, so both accept the same messages.
fn decrypt_inner_with_key(
  packets : Array[@packet.RawPacket],
  key : PrivateKey,
) -> Bytes? raise {
  let candidates : Array[@packet.SecretKeyPacket] = [key.primary]
  for c in key.subkeys {
    candidates.push(c.secret)
  }
  for p in packets {
    if p.tag() == @packet.Pkesk {
      let pkesk = @packet.PkeskPacket::parse(p.body().exact_view())
      if pkesk_is_anonymous(pkesk) {
        for sk in candidates {
          let inner : Bytes? = Some(decrypt_with_pkesk(packets, pkesk, sk)) catch {
            _ => None
          }
          if inner is Some(b) {
            return Some(b)
          }
        }
        continue
      }
      for sk in candidates {
        if pkesk_addresses(pkesk, sk) {
          return Some(decrypt_with_pkesk(packets, pkesk, sk))
        }
      }
    }
  }
  None
}

///|
/// RSA-decrypt a v3 PKESK with an unlocked RSA secret key. Returns
/// (symmetric algorithm id, session key): the cipher octet is only
/// carried inside the PKCS#1 payload (RFC 9580 §5.1.3), so it must be
/// handed back rather than assumed — GnuPG happily encrypts with
/// `--cipher-algo AES128`, and a v1 SEIPD names no cipher of its own.
///
/// Bleichenbacher countermeasure (S04): any failure inside the checked
/// path — modulus mismatch, PKCS#1 framing, payload shape, checksum —
/// yields a RANDOM session key instead of an error, mirroring Go's
/// `rsa.DecryptPKCS1v15SessionKey` as used by go-crypto. The wrong key
/// then fails uniformly inside the SEIPD integrity check, so an attacker
/// with a decryption oracle cannot distinguish where unwrapping went
/// wrong.
fn rsa_unwrap_session_key(
  pkesk : @packet.PkeskPacket,
  sk : @packet.SecretKeyPacket,
) -> (Int32, Bytes) raise {
  match rsa_unwrap_checked(pkesk, sk) {
    Some(pair) => pair
    None => {
      // A random AES-256 key of the correct length: the following SEIPD
      // v1 CFB/MDC (or v2 AEAD) open fails identically to any other
      // wrong-session-key decryption.
      let algo = @primitives.Aes256
      (algo.number(), @primitives.random_bytes(algo.key_size()))
    }
  }
}

///|
/// The real RSA PKESK unwrap; `None` marks every rejection that must be
/// masked by the random-key fallback above.
fn rsa_unwrap_checked(
  pkesk : @packet.PkeskPacket,
  sk : @packet.SecretKeyPacket,
) -> (Int32, Bytes)? raise {
  let rsa = rsa_secret_parts(sk)
  let k = rsa.public.octet_length()
  guard pkesk.encrypted().length() <= k else { return None }
  // The wire MPI may have lost a leading zero octet; os2ip restores the
  // integer value either way.
  let padded = @primitives.pad_left(pkesk.encrypted().exact_view(), k)
  let payload = rsa.pkcs1_v15_decrypt(padded.exact_view()) catch {
    _ => return None
  }
  guard payload.length() >= 3 + 16 else { return None }
  let algo = payload[0].to_int()
  guard @primitives.cipher_algo_of_number(algo) is Some(_) else { return None }
  let key_len = payload.length() - 3
  let key = payload[1:1 + key_len]
  let want = (payload[payload.length() - 2].to_int() << 8) |
    payload[payload.length() - 1].to_int()
  guard @packet.session_key_checksum(key.exact_view()) == want else {
    return None
  }
  Some((algo, key.to_owned()))
}

///|
/// Decrypt the SEIPD container in `packets` and return its raw inner packet
/// stream. A v2 container names its own cipher, so the PKESK/SKESK
/// algorithm id is unused there; a v1 container takes it from the ESK
/// packet because SEIPD v1 carries no cipher of its own.
fn unwrap_seipd_inner(
  packets : Array[@packet.RawPacket],
  algo_id : Int32,
  session_key : Bytes,
  esk_version : Int32,
) -> Bytes raise {
  for p in packets {
    if p.tag() == @packet.Seipd {
      let seipd = @packet.SeipdPacket::parse(p.body().exact_view())
      // RFC 9580 §10.3.2.1 (Table 26): "An implementation processing an
      // Encrypted Message MUST discard any preceding ESK packet with a
      // version that does not align with the version of the payload."
      // A v6 PKESK pairs with v2 SEIPD, a v3 PKESK with v1 SEIPD.
      guard (seipd.version == 2) == (esk_version == 6) else {
        @error.fail(
          "api: PKESK version \{esk_version} does not align with SEIPD version \{seipd.version}",
        )
      }
      if seipd.version == 2 {
        return open_seipd_v2(seipd, session_key)
      }
      guard seipd.version == 1 else {
        @error.fail("api: unsupported SEIPD version \{seipd.version}")
      }
      let algo = match @primitives.cipher_algo_of_number(algo_id) {
        Some(a) => a
        None => @error.fail("api: unsupported session cipher \{algo_id}")
      }
      let bc = @primitives.new_block_cipher(algo, session_key.exact_view())
      return @cipher.seipd_v1_decrypt(bc, seipd.ciphertext.exact_view())
    }
  }
  @error.fail("api: no encrypted data packet found")
}

///|
/// Decrypt an armored message with an unlocked private key.
pub fn decrypt_with_private_key(
  message : String,
  key : PrivateKey,
  max_decompressed~ : Int32 = DEFAULT_MAX_DECOMPRESSED_BYTES,
) -> Bytes raise {
  let armored = @armor.decode(message)
  decrypt_binary_with_private_key(
    @armor.ArmorBlock::data(armored),
    key,
    max_decompressed~,
  )
}

///|
/// Candidate AEAD ciphersuites for one v6 recipient, most preferred first:
/// the preferred list as given, plus the mandatory-to-implement AES-128/OCB
/// which RFC 9580 §5.2.3.15 says is "implicitly listed at the end" when the
/// subpacket does not contain it.
fn v6_ciphersuite_candidates(recipient : PublicKey) -> Array[(Int32, Int32)] {
  let out : Array[(Int32, Int32)] = []
  for pair in recipient.preferred_aead() {
    if !out.contains(pair) {
      out.push(pair)
    }
  }
  if !out.contains((7, 2)) {
    out.push((7, 2))
  }
  out
}

///|
/// Choose the (cipher, AEAD) pair for a v6 message that *every* recipient
/// accepts, in the first recipient's preference order. RFC 9580 §13.2 says
/// an implementation "MUST NOT use a symmetric algorithm that is not in the
/// recipient's preference list", so looking at only one recipient can
/// produce a message the others cannot read. Because every candidate list
/// ends with the implicit AES-128/OCB and this library implements it, a
/// common implemented pair always exists.
fn pick_v6_ciphersuite(
  recipients : Array[PublicKey],
) -> (@primitives.CipherAlgo, @cipher.AeadMode) {
  let lists : Array[Array[(Int32, Int32)]] = []
  for recipient in recipients {
    lists.push(v6_ciphersuite_candidates(recipient))
  }
  for pair in lists[0] {
    let mut accepted_by_all = true
    for i in 1.. return (c, m)
      _ => ()
    }
  }
  (@primitives.Aes128, @cipher.Ocb)
}