///|
/// Revocation certificate generation (task 2a): OpenPGP's damage-control
/// primitive — until this existed, a moonpgp user could consume key
/// revocations (`is_revoked`, verified lazily) but never create one for
/// their own key.
///
/// The hash shapes match the consumer side exactly: a primary-key
/// revocation (type 0x20) hashes like a Direct Key signature, a subkey
/// revocation (0x28) like a SubkeyBinding — both already what
/// `verify_key_signature` checks in `is_revoked` / `subkey_revoked_at`.

///|
/// Revocation reason codes (RFC 9580 §5.2.3.31). Reason 32 (User ID no
/// longer valid) belongs to certification revocations, which this library
/// does not model.
pub(all) enum RevocationReason {
  /// No reason specified.
  NoReason
  /// Replaced by a newer key.
  Superseded
  /// Secret material compromised.
  Compromised
  /// Retired; no longer used.
  Retired
} derive(Eq, Debug)

///|
pub fn RevocationReason::number(self : RevocationReason) -> Int32 {
  match self {
    NoReason => 0
    Superseded => 1
    Compromised => 2
    Retired => 3
  }
}

///|
/// The Revocation Reason subpacket body: one reason octet followed by the
/// UTF-8 message (possibly empty).
fn revocation_reason_subpacket(
  reason : RevocationReason,
  message : String?,
) -> @packet.SigSubpacket {
  let body = Bytes::from_array([reason.number().to_byte()]).add(
    @primitives.utf8_bytes(message.unwrap_or("")),
  )
  { critical: false, sub_type: @packet.SIGSUB_REVOCATION_REASON, body, }
}

///|
/// The hashed subpacket area shared by both revocation signature kinds.
fn revocation_hashed_area(
  pub_packet : @packet.PublicKeyPacket,
  now : Int32,
  reason : RevocationReason,
  message : String?,
) -> Array[@packet.SigSubpacket] raise {
  let hashed : Array[@packet.SigSubpacket] = [
    { ..creation_subpacket(now), critical: true, },
    revocation_reason_subpacket(reason, message),
  ]
  if pub_packet.version() >= 6 {
    hashed.push(issuer_fingerprint_subpacket(pub_packet))
  }
  hashed
}

///|
/// Create a primary-key revocation signature (type 0x20) signed by the
/// primary itself; hashes exactly like the Direct Key signature the
/// consumer side verifies.
fn make_key_revocation_signature(
  secret_packet : @packet.SecretKeyPacket,
  pub_packet : @packet.PublicKeyPacket,
  reason : RevocationReason,
  message : String?,
  now : Int32,
) -> @packet.SignaturePacket raise {
  let v6 = pub_packet.version() >= 6
  // v6 identifies the issuer by the fingerprint subpacket in the hashed
  // area; v4 needs the Issuer Key ID subpacket or verifiers cannot
  // attribute the signature at all (caught by cross-checking with gpg,
  // which silently ignored the issuerless revocation).
  let unhashed : Array[@packet.SigSubpacket] = if v6 {
    []
  } else {
    [
      {
        critical: false,
        sub_type: @packet.SIGSUB_ISSUER_KEY_ID,
        body: pub_packet.key_id(),
      },
    ]
  }
  let sig : @packet.SignaturePacket = {
    version: if v6 {
      6
    } else {
      4
    },
    sig_type: @packet.KeyRevocation,
    pub_algo: secret_packet.public.algo(),
    hash_algo: @primitives.Sha256.number(),
    hashed_subpackets: revocation_hashed_area(pub_packet, now, reason, message),
    unhashed_subpackets: unhashed,
    raw_hashed_area: Bytes::new(0),
    fast_hash: 0,
    salt: if v6 {
      @primitives.random_bytes(
        @packet.salt_size_of_hash(@primitives.Sha256.number()),
      )
    } else {
      Bytes::new(0)
    },
    sig_params: [],
  }
  let digest = hash_direct_key(sig, pub_packet)
  finish_signature(sig, secret_packet, @primitives.Sha256, digest.exact_view())
}

///|
/// Create a subkey revocation signature (type 0x28) signed by the primary;
/// hashes exactly like the subkey binding the consumer side verifies.
fn make_subkey_revocation_signature(
  secret_packet : @packet.SecretKeyPacket,
  primary_pub : @packet.PublicKeyPacket,
  subkey_pub : @packet.PublicKeyPacket,
  reason : RevocationReason,
  message : String?,
  now : Int32,
) -> @packet.SignaturePacket raise {
  let v6 = primary_pub.version() >= 6
  // The issuer of a subkey revocation is the PRIMARY key (see the v4
  // issuer note in make_key_revocation_signature).
  let unhashed : Array[@packet.SigSubpacket] = if v6 {
    []
  } else {
    [
      {
        critical: false,
        sub_type: @packet.SIGSUB_ISSUER_KEY_ID,
        body: primary_pub.key_id(),
      },
    ]
  }
  let sig : @packet.SignaturePacket = {
    version: if v6 {
      6
    } else {
      4
    },
    sig_type: @packet.SubkeyRevocation,
    pub_algo: secret_packet.public.algo(),
    hash_algo: @primitives.Sha256.number(),
    hashed_subpackets: revocation_hashed_area(primary_pub, now, reason, message),
    unhashed_subpackets: unhashed,
    raw_hashed_area: Bytes::new(0),
    fast_hash: 0,
    salt: if v6 {
      @primitives.random_bytes(
        @packet.salt_size_of_hash(@primitives.Sha256.number()),
      )
    } else {
      Bytes::new(0)
    },
    sig_params: [],
  }
  let digest = hash_binding(sig, primary_pub, subkey_pub)
  finish_signature(sig, secret_packet, @primitives.Sha256, digest.exact_view())
}

///|
/// Generate, but do not apply, a standalone revocation certificate for the
/// primary key: a single key-revocation signature packet in PUBLIC KEY
/// BLOCK armor — the `.rev` shape `gpg --gen-revoke` emits (gpg refuses to
/// --import a bare Signature armor block, verified against 2.4.5). The
/// owner distributes it out of band; anyone can apply it to their copy of
/// the public key.
///
/// # Example
///
/// ```mbt check
/// test "a revocation certificate is armored" {
///   let key = @api.generate_ed25519_key("A ")
///   let cert = @api.generate_revocation_certificate(key)
///   inspect(
///     cert.has_prefix("-----BEGIN PGP PUBLIC KEY BLOCK-----"),
///     content="true",
///   )
/// }
/// ```
pub fn generate_revocation_certificate(
  key : PrivateKey,
  reason~ : RevocationReason = NoReason,
  message? : String,
  created? : Int32,
) -> String raise {
  let now = match created {
    Some(t) => t
    None => @primitives.unix_now().to_int()
  }
  let sig = make_key_revocation_signature(
    key.primary,
    key.primary.public,
    reason,
    message,
    now,
  )
  @armor.PublicKey.encode(
    @packet.serialize_packet(@packet.Signature, sig.to_body().exact_view()).exact_view(),
  )
}

///|
/// Return a copy of `key` with a fresh primary-key revocation appended;
/// the certificate round-trips with the revocation intact and every
/// lifecycle gate (`is_revoked`, `can_encrypt`, `can_verify`, signature
/// verification) sees the key as revoked.
pub fn revoke_key(
  key : PrivateKey,
  reason~ : RevocationReason = NoReason,
  message? : String,
  created? : Int32,
) -> PrivateKey raise {
  let now = match created {
    Some(t) => t
    None => @primitives.unix_now().to_int()
  }
  let sig = make_key_revocation_signature(
    key.primary,
    key.primary.public,
    reason,
    message,
    now,
  )
  let revocations = key.revocations.copy()
  revocations.push(sig)
  { ..key, revocations, }
}

///|
/// Return a copy of `key` with a revocation signature on the subkey whose
/// key id matches `subkey_id`. A revoked subkey drops out of encryption
/// selection immediately; decryption with it stays possible (mail that
/// arrived before the revocation must keep decrypting, mirroring
/// `KeyRing::find_decryption_key`).
pub fn revoke_subkey(
  key : PrivateKey,
  subkey_id : BytesView,
  reason~ : RevocationReason = NoReason,
  message? : String,
  created? : Int32,
) -> PrivateKey raise {
  let now = match created {
    Some(t) => t
    None => @primitives.unix_now().to_int()
  }
  let subkeys : Array[SecretSubkeyCert] = []
  let mut found = false
  for c in key.subkeys {
    let id = c.secret.public.key_id() catch { _ => Bytes::new(0) }
    if !found && id.length() > 0 && id.equal(subkey_id.to_owned()) {
      let sig = make_subkey_revocation_signature(
        key.primary,
        key.primary.public,
        c.secret.public,
        reason,
        message,
        now,
      )
      subkeys.push({ ..c, revocation: Some(sig), })
      found = true
    } else {
      subkeys.push(c)
    }
  }
  guard found else {
    @error.fail("api: no subkey of this key matches the given key id")
  }
  { ..key, subkeys, }
}

///|
/// Parse an armored standalone revocation certificate, check it is a
/// primary-key revocation that cryptographically verifies over this key,
/// and return a copy with it appended. Anything else (wrong armor type, a
/// non-revocation signature, a signature for another key) is an error —
/// an invalid revocation must not be able to disable a key.
pub fn PrivateKey::apply_revocation_certificate(
  self : PrivateKey,
  armored : String,
) -> PrivateKey raise {
  let sig = parse_revocation_certificate(armored)
  let ok = verify_key_signature(
    sig,
    self.primary.public,
    self.primary.public,
    direct=true,
  ) catch {
    _ => false
  }
  guard ok else {
    @error.fail("api: the revocation certificate does not verify over this key")
  }
  let revocations = self.revocations.copy()
  revocations.push(sig)
  { ..self, revocations, }
}

///|
/// The public-half twin of `PrivateKey::apply_revocation_certificate`.
pub fn PublicKey::apply_revocation_certificate(
  self : PublicKey,
  armored : String,
) -> PublicKey raise {
  let sig = parse_revocation_certificate(armored)
  let ok = verify_key_signature(sig, self.primary, self.primary, direct=true) catch {
    _ => false
  }
  guard ok else {
    @error.fail("api: the revocation certificate does not verify over this key")
  }
  let revocations = self.revocations.copy()
  revocations.push(sig)
  { ..self, revocations, }
}

///|
/// Decode a standalone revocation certificate. The label is not load
/// bearing — gpg writes `.rev` files as PUBLIC KEY BLOCK armor holding
/// just the signature packet, while a Signature-labeled block is equally
/// valid — so both are accepted and the packet inside decides.
fn parse_revocation_certificate(
  armored : String,
) -> @packet.SignaturePacket raise {
  let block = @armor.decode(armored)
  guard block.armor_type() == @armor.Signature ||
    block.armor_type() == @armor.PublicKey else {
    @error.fail("api: not a revocation certificate armor block")
  }
  let packets = @packet.parse_packets(block.data().exact_view())
  let mut sig : @packet.SignaturePacket? = None
  for p in packets {
    if p.tag() == @packet.Signature {
      sig = Some(@packet.SignaturePacket::parse(p.body().exact_view()))
      break
    }
  }
  let s = match sig {
    Some(s) => s
    None => @error.fail("api: no signature packet in armor block")
  }
  guard s.sig_type() == @packet.KeyRevocation else {
    @error.malformed(
      "api: the certificate is not a key revocation (type \{s.sig_type().number()})",
    )
  }
  s
}

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

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