///|
/// Session-key entry points (M3), mirroring gopenpgp's `crypto.SessionKey`
/// and its `EncryptSessionKey`/`DecryptSessionKey` flows. Session keys
/// normally stay internal to `encrypt_to_recipients`/`encrypt_with_password`;
/// these exist for the out-of-band uses โ€” sealing a message to a session
/// key the caller already holds (no ESK packets on the wire) and
/// recovering a session key from a message so it can be re-wrapped or
/// stored elsewhere.

///|
/// A symmetric session key with the cipher that interprets it. `key`
/// must be `algo.key_size()` octets โ€” `SessionKey::new` enforces it;
/// direct struct construction bypasses the check and fails at first use
/// inside the block-cipher constructor.
pub(all) struct SessionKey {
  key : Bytes
  algo : @primitives.CipherAlgo
} derive(Debug)

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

///|
/// Build a session key, validating the key length against the cipher.
pub fn SessionKey::new(
  key : BytesView,
  algo : @primitives.CipherAlgo,
) -> SessionKey raise {
  guard key.length() == algo.key_size() else {
    @error.fail(
      "api: session key length \{key.length()} does not match the cipher's \{algo.key_size()}",
    )
  }
  SessionKey::{ key: key.to_owned(), algo, }
}

///|
/// The raw session-key octets.
pub fn SessionKey::key(self : SessionKey) -> Bytes {
  self.key
}

///|
/// The cipher this session key belongs to.
pub fn SessionKey::algo(self : SessionKey) -> @primitives.CipherAlgo {
  self.algo
}

///|
/// Generate a fresh random session key (platform entropy; AES-256 unless
/// another cipher is requested).
pub fn generate_session_key(
  algo~ : @primitives.CipherAlgo = @primitives.Aes256,
) -> SessionKey raise {
  SessionKey::new(@primitives.random_bytes(algo.key_size()), algo)
}

///|
/// Encrypt plaintext under a caller-held session key: a bare SEIPD v1
/// container with no PKESK/SKESK โ€” only whoever holds `sk` out of band
/// can read it (the gopenpgp "raw session key" backend).
pub fn encrypt_to_session_key(
  plaintext : BytesView,
  sk : SessionKey,
  file_name? : String,
  compress? : @compress.CompressedAlgo,
) -> String raise {
  let literal = @packet.LiteralPacket::binary(plaintext, file_name?)
  let inner = @packet.serialize_packet(
    @packet.Literal,
    literal.to_body().exact_view(),
  )
  let inner = maybe_compress_inner(inner, compress?)
  let bc = @primitives.new_block_cipher(sk.algo, sk.key.exact_view())
  let ct = @cipher.seipd_v1_encrypt(bc, inner.exact_view())
  let seipd : @packet.SeipdPacket = {
    version: 1,
    cipher_algo: 0,
    aead_algo: 0,
    chunk_octet: 0,
    salt: Bytes::new(0),
    ciphertext: ct,
  }
  @armor.Message.encode(
    @packet.serialize_packet(@packet.Seipd, seipd.to_body().exact_view()).exact_view(),
  )
}

///|
/// Decrypt a message with a caller-held session key. The message may be
/// a bare SEIPD container (from `encrypt_to_session_key`) or a normal
/// PKESK/SKESK-protected message whose session key the caller knows.
pub fn decrypt_to_session_key(message : String, sk : SessionKey) -> Bytes raise {
  let block = @armor.decode(message)
  let packets = @packet.parse_packets(
    @armor.ArmorBlock::data(block).exact_view(),
  )
  let bc = @primitives.new_block_cipher(sk.algo, sk.key.exact_view())
  for p in packets {
    if p.tag() == @packet.Seipd {
      let seipd = @packet.SeipdPacket::parse(p.body().exact_view())
      if seipd.version == 1 {
        return extract_literal(
          @cipher.seipd_v1_decrypt(bc, seipd.ciphertext.exact_view()),
          DEFAULT_MAX_DECOMPRESSED_BYTES,
        )
      }
      return extract_literal(
        open_seipd_v2(seipd, sk.key),
        DEFAULT_MAX_DECOMPRESSED_BYTES,
      )
    }
  }
  @error.fail("api: no SEIPD packet found")
}

///|
/// Recover the session key of a password-encrypted message (the
/// gopenpgp `RetrieveSessionKey` flow): unwrap the SKESK without
/// touching the payload.
pub fn recover_session_key_with_password(
  message : String,
  passphrase : String,
  argon2_max_memory_kib~ : Int32 = @primitives.DEFAULT_ARGON2_MAX_MEMORY_KIB,
) -> SessionKey raise {
  let block = @armor.decode(message)
  let packets = @packet.parse_packets(
    @armor.ArmorBlock::data(block).exact_view(),
  )
  let (algo, key, _) = unwrap_skesk(packets, passphrase, argon2_max_memory_kib)
  SessionKey::new(key, algo)
}

///|
/// Recover the session key of a message encrypted to `key`'s public key:
/// unwrap the matching PKESK without decrypting the payload. The
/// algorithm id comes from the PKESK payload for the v4 shape and from
/// the SEIPD v2 header for v6.
pub fn recover_session_key_with_key(
  message : String,
  key : PrivateKey,
) -> SessionKey raise {
  let block = @armor.decode(message)
  let packets = @packet.parse_packets(
    @armor.ArmorBlock::data(block).exact_view(),
  )
  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())
      // Hidden recipient (S13): an all-zero v3 key id matches no key by
      // id, so every candidate is tried; a failing candidate is just not
      // the recipient.
      if pkesk_is_anonymous(pkesk) {
        for sk in candidates {
          let recovered : SessionKey? = Some(
            recover_pkesk_session(packets, pkesk, sk),
          ) catch {
            _ => None
          }
          if recovered is Some(s) {
            return s
          }
        }
        continue
      }
      for sk in candidates {
        if pkesk_addresses(pkesk, sk) {
          return recover_pkesk_session(packets, pkesk, sk)
        }
      }
    }
  }
  @error.fail("api: message is not encrypted to this key")
}

///|
/// Unwrap one PKESK into a session key: the algorithm id comes from the
/// PKESK payload for the v3 shape and from the SEIPD v2 header for v6.
fn recover_pkesk_session(
  packets : Array[@packet.RawPacket],
  pkesk : @packet.PkeskPacket,
  sk : @packet.SecretKeyPacket,
) -> SessionKey raise {
  let (algo_id, session) = unwrap_pkesk(pkesk, sk)
  if pkesk.version() == 6 {
    // A v6 PKESK carries no cipher octet; the v2 SEIPD names it.
    for q in packets {
      if q.tag() == @packet.Seipd {
        let seipd = @packet.SeipdPacket::parse(q.body().exact_view())
        guard seipd.version == 2 else {
          @error.fail(
            "api: v6 PKESK must precede a v2 SEIPD (RFC 9580 ยง10.3.2.1)",
          )
        }
        let algo = match @primitives.cipher_algo_of_number(seipd.cipher_algo) {
          Some(a) => a
          None =>
            @error.fail("api: unsupported session cipher \{seipd.cipher_algo}")
        }
        return SessionKey::new(session, algo)
      }
    }
    @error.fail("api: no SEIPD packet found")
  }
  let algo = match @primitives.cipher_algo_of_number(algo_id) {
    Some(a) => a
    None => @error.fail("api: unsupported session cipher \{algo_id}")
  }
  SessionKey::new(session, algo)
}