///|
/// Public key algorithm IDs (RFC 9580 §9.2).
pub(all) enum PubKeyAlgo {
  RsaEncryptOrSign
  RsaEncryptOnly
  RsaSignOnly
  Dsa
  Ecdh
  Ecdsa
  EdDsaLegacy
  X25519
  Ed25519
  Unknown(Int32)
} derive(Eq, Debug)

///|
pub fn PubKeyAlgo::number(self : PubKeyAlgo) -> Int32 {
  match self {
    RsaEncryptOrSign => 1
    RsaEncryptOnly => 2
    RsaSignOnly => 3
    Dsa => 17
    Ecdh => 18
    Ecdsa => 19
    EdDsaLegacy => 22
    X25519 => 25
    Ed25519 => 27
    Unknown(n) => n
  }
}

///|
pub fn pub_key_algo_of_number(n : Int32) -> PubKeyAlgo {
  match n {
    1 => RsaEncryptOrSign
    2 => RsaEncryptOnly
    3 => RsaSignOnly
    17 => Dsa
    18 => Ecdh
    19 => Ecdsa
    22 => EdDsaLegacy
    25 => X25519
    27 => Ed25519
    _ => Unknown(n)
  }
}

///|
pub fn PubKeyAlgo::is_rsa(self : PubKeyAlgo) -> Bool {
  match self {
    RsaEncryptOrSign | RsaEncryptOnly | RsaSignOnly => true
    _ => false
  }
}

///|
/// A version 4 (or 5) public key packet body (RFC 9580 §5.5.2).
pub(all) struct PublicKeyPacket {
  version : Int32
  /// Seconds since the Unix epoch.
  creation_time : Int32
  algo : PubKeyAlgo
  /// Algorithm-specific public parameters in wire order (MPI bodies for
  /// RSA: n then e).
  params : Array[Bytes]
} derive(Debug)

///|
pub fn PublicKeyPacket::version(self : PublicKeyPacket) -> Int32 {
  self.version
}

///|
pub fn PublicKeyPacket::creation_time(self : PublicKeyPacket) -> Int32 {
  self.creation_time
}

///|
pub fn PublicKeyPacket::algo(self : PublicKeyPacket) -> PubKeyAlgo {
  self.algo
}

///|
pub fn PublicKeyPacket::params(self : PublicKeyPacket) -> Array[Bytes] {
  self.params
}

///|
/// Parse the version/creation-time/algorithm/params fields at the front of
/// a public key body (F13: the single source of the field walk, shared by
/// `PublicKeyPacket::parse` and `public_part_length` so the two cannot
/// drift apart when an algorithm is added or changed).
fn read_public_fields(
  r : Reader,
) -> (Int32, Int32, PubKeyAlgo, Array[Bytes]) raise {
  let version = r.read_u8()
  // v5 is the abandoned 4880bis draft shape: nothing else here
  // understands it (fingerprints, PKESK pairing), so reject it at the
  // parse layer instead of accepting a key that fails at first use (N8).
  guard version == 4 || version == 6 else {
    @error.fail(
      "packet: unsupported public key version \{version} (v5 draft keys are not supported)",
    )
  }
  let creation_time = r.read_u32()
  let algo_num = r.read_u8()
  let algo = pub_key_algo_of_number(algo_num)
  let params : Array[Bytes] = []
  if version == 6 {
    // Version 6 carries a 4-octet count of the algorithm-specific
    // material (RFC 9580 §5.5.2.3); native algorithms are raw octet
    // strings, not MPIs.
    let count = r.read_u32()
    let material_start = r.position()
    match algo {
      RsaEncryptOrSign | RsaEncryptOnly | RsaSignOnly => {
        params.push(r.read_mpi())
        params.push(r.read_mpi())
      }
      Ed25519 =>
        // 32 octets of the native Ed25519 public key (RFC 9580 §5.5.5.9).
        params.push(r.read_bytes(32))
      X25519 =>
        // 32 octets of the native X25519 public key (RFC 9580 §5.5.5.7).
        params.push(r.read_bytes(32))
      _ =>
        @error.fail("packet: v6 public key algorithm \{algo_num} not supported")
    }
    guard r.position() - material_start == count else {
      @error.fail("packet: v6 public key material count does not match body")
    }
  } else {
    match algo {
      RsaEncryptOrSign | RsaEncryptOnly | RsaSignOnly => {
        params.push(r.read_mpi())
        params.push(r.read_mpi())
      }
      Ed25519 | X25519 =>
        // The native algorithms are the same 32 raw octets in v4 keys:
        // §5.5.2.2 defers the material to §5.5.5, which defines 32 native
        // octets with no version restriction — only the v6 4-octet count
        // (and the MPI framing of EdDsaLegacy/Ecdh below) is v4-absent.
        params.push(r.read_bytes(32))
      EdDsaLegacy => {
        // First param: the length-prefixed curve OID, kept verbatim
        // (leading octet is the OID length). Second: the MPI point.
        let oid_len = r.read_u8()
        let oid = r.read_bytes(oid_len)
        params.push(Bytes::from_array([oid_len.to_byte()]).add(oid))
        params.push(r.read_mpi())
      }
      Ecdh => {
        // Params in wire order (RFC 9580 §5.5.5.6): length-prefixed curve
        // OID, MPI point, length-prefixed KDF parameters, each kept
        // verbatim.
        let oid_len = r.read_u8()
        let oid = r.read_bytes(oid_len)
        params.push(Bytes::from_array([oid_len.to_byte()]).add(oid))
        params.push(r.read_mpi())
        let kdf_len = r.read_u8()
        let kdf = r.read_bytes(kdf_len)
        params.push(Bytes::from_array([kdf_len.to_byte()]).add(kdf))
      }
      _ => @error.fail("packet: unsupported public key algorithm \{algo_num}")
    }
  }
  (version, creation_time, algo, params)
}

///|
pub fn PublicKeyPacket::parse(body : BytesView) -> PublicKeyPacket raise {
  let r = Reader::new(body)
  let (version, creation_time, algo, params) = read_public_fields(r)
  PublicKeyPacket::{ version, creation_time, algo, params, }
}

///|
pub fn PublicKeyPacket::to_body(self : PublicKeyPacket) -> Bytes {
  let w = Writer::new()
  w.u8(self.version)
  w.u32(self.creation_time)
  w.u8(self.algo.number())
  if self.version >= 6 {
    // 4-octet count of the algorithm-specific material, then the raw
    // (native) or MPI-encoded fields.
    let material = Writer::new()
    match self.algo {
      Ed25519 | X25519 =>
        for p in self.params {
          material.bytes(p.exact_view())
        }
      _ =>
        for p in self.params {
          material.mpi(p.exact_view())
        }
    }
    let mb = material.to_bytes()
    w.u32(mb.length())
    w.bytes(mb.exact_view())
  } else {
    match self.algo {
      Ed25519 | X25519 =>
        // v4 native keys carry the 32 raw octets, no MPI framing.
        for p in self.params {
          w.bytes(p.exact_view())
        }
      EdDsaLegacy => {
        // First param carries the length-prefixed OID verbatim.
        w.bytes(self.params[0].exact_view())
        w.mpi(self.params[1].exact_view())
      }
      Ecdh => {
        // OID and KDF params carry their own length octets; point is an MPI.
        w.bytes(self.params[0].exact_view())
        w.mpi(self.params[1].exact_view())
        w.bytes(self.params[2].exact_view())
      }
      _ =>
        for p in self.params {
          w.mpi(p.exact_view())
        }
    }
  }
  w.to_bytes()
}

///|
/// Fingerprint: v4 = SHA-1 over 0x99, 2-octet length, key body (20
/// octets); v6 = SHA-256 over 0x9B, 4-octet length, key body (32 octets)
/// (RFC 9580 §5.5.4.3).
pub fn PublicKeyPacket::fingerprint(self : PublicKeyPacket) -> Bytes raise {
  let body = self.to_body()
  let w = Writer::new()
  if self.version >= 6 {
    w.u8(0x9B)
    w.u32(body.length())
    w.bytes(body.exact_view())
    @primitives.Sha256.digest(w.to_bytes().exact_view())
  } else if self.version == 4 {
    // F09: the u16 length field silently truncates an oversized body.
    guard body.length() <= 0xFFFF else {
      @error.fail("packet: key packet too large for a v4 fingerprint")
    }
    w.u8(0x99)
    w.u16(body.length())
    w.bytes(body.exact_view())
    @primitives.Sha1.digest(w.to_bytes().exact_view())
  } else {
    @error.fail(
      "packet: fingerprint not implemented for version \{self.version}",
    )
  }
}

///|
/// The 8-octet (64-bit) key id derived from the fingerprint: the low 64
/// bits for v4, the HIGH 64 bits for v6 (RFC 9580 §5.5.4.3).
pub fn PublicKeyPacket::key_id(self : PublicKeyPacket) -> Bytes raise {
  let fp = self.fingerprint()
  if self.version >= 6 {
    fp[0:8].to_owned()
  } else {
    fp[fp.length() - 8:fp.length()].to_owned()
  }
}

///|
/// A version 4 or 6 secret key packet: public part plus secret material.
/// The secret material is kept in wire form; unlocking/locking happens at
/// the key-management layer.
pub(all) struct SecretKeyPacket {
  public : PublicKeyPacket
  /// S2K usage: 0 = plaintext secret, 253 = AEAD + HKDF key separation,
  /// 254 = CFB + SHA-1 check, 255 = CFB + 2-octet checksum, other = legacy
  /// cipher id + checksum.
  s2k_usage : Int32
  /// Cipher algorithm id when the secret material is encrypted (0 otherwise).
  cipher_algo : Int32
  /// AEAD algorithm id for usage 253 (0 otherwise).
  aead_algo : Int32
  /// S2K specifier bytes when encrypted.
  s2k_spec : Bytes
  /// IV (or AEAD nonce) bytes when encrypted.
  iv : Bytes
  /// The (possibly still encrypted) algorithm-specific secret material,
  /// exactly as found on the wire after the S2K/IV fields. For usage 253
  /// this includes the trailing AEAD authentication tag.
  secret_material : Bytes
  /// True for GnuPG "GNU dummy" keys whose secret material lives on a
  /// smartcard (S2K type 101 marker); there is nothing to unlock.
  dummy : Bool
} derive(Debug)

///|
/// Read an S2K specifier from `r`, honouring the v6 length octet, and
/// return both the parsed specifier and its raw wire bytes.
fn read_s2k_specifier(r : Reader, v6 : Bool) -> (@s2k.S2k, Bytes) raise {
  if v6 {
    // RFC 9580 §5.5.3: v6 prefixes the specifier with a 1-octet count of
    // the field that follows it (usage 253 or 254 only).
    let declared = r.read_u8()
    guard rest_after(r, declared) else {
      @error.fail("packet: truncated v6 S2K specifier")
    }
    let (s2k, used) = @s2k.parse(r.view()[0:declared])
    guard used == declared else {
      @error.fail("packet: v6 S2K specifier length mismatch")
    }
    (s2k, r.read_bytes(declared))
  } else {
    let (s2k, used) = @s2k.parse(r.view())
    (s2k, r.read_bytes(used))
  }
}

///|
pub fn SecretKeyPacket::parse(body : BytesView) -> SecretKeyPacket raise {
  // The public part length is needed to hash/serialize; parse it first.
  let pub_len = public_part_length(body)
  let public = PublicKeyPacket::parse(body[0:pub_len])
  let v6 = public.version() >= 6
  let r = Reader::new(body[pub_len:body.length()])
  let usage = r.read_u8()
  if usage == 0 {
    // v6 carries no trailing 2-octet checksum; the material is raw.
    SecretKeyPacket::{
      public,
      s2k_usage: 0,
      cipher_algo: 0,
      aead_algo: 0,
      s2k_spec: Bytes::new(0),
      iv: Bytes::new(0),
      secret_material: r.read_rest(),
      dummy: false,
    }
  } else if usage == 253 {
    // RFC 9580 §5.5.3: AEAD-protected secret key material. This is the only
    // usage octet that may carry an Argon2 specifier for a secret key.
    // The 1-octet count covers every field that follows it, so it must be
    // compared against the octets actually consumed (a "enough bytes left"
    // guard alone would accept a wrong count and split the fields by luck).
    let mut declared_params : Int32? = None
    let mut params_start = 0
    if v6 {
      let params_len = r.read_u8()
      guard rest_after(r, params_len) else {
        @error.fail("packet: truncated v6 AEAD secret key parameters")
      }
      declared_params = Some(params_len)
      params_start = r.position()
    }
    let cipher_algo = r.read_u8()
    let aead_algo = r.read_u8()
    let (_s2k, spec) = read_s2k_specifier(r, v6)
    let iv = r.read_bytes(aead_iv_len(aead_algo))
    match declared_params {
      Some(n) =>
        if r.position() - params_start != n {
          @error.fail(
            "packet: v6 AEAD secret key parameter count \{n} does not match the \{r.position() - params_start} octets read",
          )
        }
      None => ()
    }
    SecretKeyPacket::{
      public,
      s2k_usage: usage,
      cipher_algo,
      aead_algo,
      s2k_spec: spec,
      iv,
      secret_material: r.read_rest(),
      dummy: false,
    }
  } else if usage == 254 || usage == 255 {
    guard !(v6 && usage == 255) else {
      @error.fail("packet: a v6 secret key must not use S2K usage 255")
    }
    let mut declared_params : Int32? = None
    let mut params_start = 0
    if v6 {
      let params_len = r.read_u8()
      guard rest_after(r, params_len) else {
        @error.fail("packet: truncated v6 secret key parameters")
      }
      declared_params = Some(params_len)
      params_start = r.position()
    }
    let cipher_algo = r.read_u8()
    // GnuPG dummy marker: cipher 0 and the S2K "GNU" extension
    // (specifier type 101, hash 0, "GNU", sub-type).
    let rest = r.view()
    if cipher_algo == 0 &&
      rest.length() >= 6 &&
      rest[0].to_int() == 101 &&
      rest[2].to_int() == 71 &&
      rest[3].to_int() == 78 &&
      rest[4].to_int() == 85 {
      SecretKeyPacket::{
        public,
        s2k_usage: usage,
        cipher_algo: 0,
        aead_algo: 0,
        s2k_spec: r.read_rest(),
        iv: Bytes::new(0),
        secret_material: Bytes::new(0),
        dummy: true,
      }
    } else {
      let (s2k, spec) = read_s2k_specifier(r, v6)
      guard !s2k.is_argon2() else {
        @error.fail("packet: using Argon2 S2K without AEAD is not allowed")
      }
      let bs = cipher_block_size(cipher_algo)
      let iv = r.read_bytes(bs)
      // Same exact-consumption check as usage 253. The GnuPG "GNU dummy"
      // branch above deliberately keeps the lenient treatment: its layout
      // ends in an implementation-defined extension we must not second-guess.
      match declared_params {
        Some(n) =>
          if r.position() - params_start != n {
            @error.fail(
              "packet: v6 secret key parameter count \{n} does not match the \{r.position() - params_start} octets read",
            )
          }
        None => ()
      }
      SecretKeyPacket::{
        public,
        s2k_usage: usage,
        cipher_algo,
        aead_algo: 0,
        s2k_spec: spec,
        iv,
        secret_material: r.read_rest(),
        dummy: false,
      }
    }
  } else {
    // RFC 9580 §3.7.2.1: the cipher-id usage rows are for reading version
    // 4 and earlier keys only; "Version 6 secret keys using these formats
    // MUST be rejected" rather than accepted with an implied Simple/MD5.
    guard !v6 else {
      @error.fail(
        "packet: a v6 secret key must not use a legacy cipher-id usage octet \{usage}",
      )
    }
    // Legacy: the usage octet is itself the cipher algorithm id and a
    // simple S2K (MD5) is implied.
    let cipher_algo = usage
    let simple_spec = Bytes::from_array([0, 1])
    let bs = cipher_block_size(cipher_algo)
    let iv = r.read_bytes(bs)
    SecretKeyPacket::{
      public,
      s2k_usage: usage,
      cipher_algo,
      aead_algo: 0,
      s2k_spec: simple_spec,
      iv,
      secret_material: r.read_rest(),
      dummy: false,
    }
  }
}

///|
/// True when at least `n` bytes remain in the reader.
fn rest_after(r : Reader, n : Int32) -> Bool {
  r.view().length() >= n
}

///|
/// The length of the public-key part at the front of a secret key body:
/// walk it with the shared field reader (no trailing-data requirement).
fn public_part_length(body : BytesView) -> Int32 raise {
  let r = Reader::new(body)
  let _fields = read_public_fields(r)
  r.position()
}

///|
/// Block size of a symmetric algorithm id (only what v4 key protection
/// needs). Returns 16 for unknown ids of the AES family and 8 for legacy
/// 64-bit ciphers.
fn cipher_block_size(algo : Int32) -> Int32 raise {
  match algo {
    1 => 8
    2 | 3 | 4 => 8
    7 | 8 | 9 | 10 | 11 | 12 | 13 => 16
    _ => @error.fail("packet: unknown cipher algorithm \{algo}")
  }
}

///|
/// The cumulative octet count of the v6 S2K parameter fields that follow
/// the count octet itself (RFC 9580 §5.5.3): cipher, optional AEAD mode,
/// the optional S2K-spec length octet, the specifier, and the IV.
fn v6_params_length(key : SecretKeyPacket) -> Int32 {
  let mut n = 1 // symmetric cipher octet
  if key.s2k_usage == 253 {
    n = n + 1 // AEAD mode octet
  }
  n = n + 1 + key.s2k_spec.length() // S2K spec length octet + specifier
  n = n + key.iv.length()
  n
}

///|
/// Serialize: public body followed by the secret-material wire bytes.
///
/// The caller is responsible for the RFC 9580 §5.5.3 generation rules that
/// `parse` enforces (no usage 255 on a v6 packet, and a v6 S2K parameter
/// block that fits its 1-octet count); `api.lock_secret_key` is the only
/// producer of protected packets and validates them there.
pub fn SecretKeyPacket::to_body(self : SecretKeyPacket) -> Bytes {
  let w = Writer::new()
  w.bytes(self.public.to_body().exact_view())
  let v6 = self.public.version() >= 6
  // Each branch writes its own usage octet. For the legacy form that octet
  // IS the cipher id, and writing it twice shifted the IV by one octet on
  // every re-serialization (import -> export corruption).
  if self.s2k_usage == 0 {
    w.u8(0)
    w.bytes(self.secret_material.exact_view())
  } else if self.s2k_usage == 253 {
    w.u8(253)
    if v6 {
      w.u8(v6_params_length(self))
    }
    w.u8(self.cipher_algo)
    w.u8(self.aead_algo)
    if v6 {
      w.u8(self.s2k_spec.length())
    }
    w.bytes(self.s2k_spec.exact_view())
    w.bytes(self.iv.exact_view())
    w.bytes(self.secret_material.exact_view())
  } else if self.s2k_usage == 254 || self.s2k_usage == 255 {
    w.u8(self.s2k_usage)
    if v6 {
      w.u8(v6_params_length(self))
    }
    w.u8(self.cipher_algo)
    if v6 {
      // RFC 9580 §5.5.3: the S2K-spec length octet exists for usage 253
      // and 254 only.
      w.u8(self.s2k_spec.length())
    }
    w.bytes(self.s2k_spec.exact_view())
    w.bytes(self.iv.exact_view())
    w.bytes(self.secret_material.exact_view())
  } else {
    // Legacy usage octet is the cipher id; IV follows, simple S2K implied.
    w.u8(self.cipher_algo)
    w.bytes(self.iv.exact_view())
    w.bytes(self.secret_material.exact_view())
  }
  w.to_bytes()
}

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

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

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

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