///|
/// Canonicalize text for text signatures (type 0x01): line endings become
/// CRLF (RFC 9580 §5.2.4).
fn canonicalize_text(data : BytesView) -> Bytes {
  let out : Array[Byte] = []
  let mut prev_cr = false
  for b in data {
    if b == b'\n' {
      if !prev_cr {
        out.push(b'\r')
      }
      out.push(b'\n')
      prev_cr = false
    } else {
      if b == b'\r' {
        prev_cr = true
      } else {
        prev_cr = false
      }
      out.push(b)
    }
  }
  Bytes::from_array(out)
}

///|
/// Document bytes hashed for a signature over `data` with sig type 0x00
/// (binary) or 0x01 (canonical text). Version 6 prepends the salt to the
/// hash input (RFC 9580 §5.2.4) and uses a 4-octet trailer length.
fn document_hash(
  sig : @packet.SignaturePacket,
  data : BytesView,
  sig_type : @packet.SigType,
) -> Bytes raise {
  let content : Bytes = match sig_type {
    @packet.CanonicalTextDocument => canonicalize_text(data)
    _ => data.to_owned()
  }
  finish_signed_digest(sig, content.exact_view())
}

///|
/// Hash input assembly shared by document and key signatures: the salt
/// (v6 only, before everything else), the framed data, the hash prefix
/// and the version-appropriate trailer.
fn finish_signed_digest(
  sig : @packet.SignaturePacket,
  data : BytesView,
) -> Bytes raise {
  let w = @packet.Writer::new()
  w.bytes(sig.salt().exact_view())
  w.bytes(data)
  w.bytes(sig.hash_prefix().exact_view())
  w.bytes(
    @packet.signature_trailer(sig.version(), sig.hash_prefix().length()).exact_view(),
  )
  match @primitives.hash_id_of_number(sig.hash_algo()) {
    Some(h) => h.digest(w.to_bytes().exact_view())
    None => @error.fail("api: unknown hash algorithm")
  }
}

///|
/// Issuer Fingerprint subpacket body: version octet || fingerprint
/// (RFC 9580 §5.2.3.33). This is how v6 signatures identify their key —
/// the Issuer Key ID subpacket MUST NOT be used for keys newer than v4.
fn issuer_fingerprint_subpacket(
  pub_packet : @packet.PublicKeyPacket,
) -> @packet.SigSubpacket raise {
  let fp = pub_packet.fingerprint()
  let w = @packet.Writer::new()
  w.u8(pub_packet.version())
  w.bytes(fp.exact_view())
  {
    critical: false,
    sub_type: @packet.SIGSUB_ISSUER_FINGERPRINT,
    body: w.to_bytes(),
  }
}

///|
/// Build a v4 document signature with the given type and default hash.
fn make_document_signature(
  key : PrivateKey,
  data : BytesView,
  sig_type : @packet.SigType,
  created? : Int32,
) -> @packet.SignaturePacket raise {
  make_document_signature_with(
    key,
    data,
    sig_type,
    @primitives.Sha256,
    created?,
  )
}

///|
/// Build a v4 text signature with an explicit hash algorithm.
fn make_text_signature(
  key : PrivateKey,
  data : BytesView,
  hash : @primitives.HashId,
  created? : Int32,
) -> @packet.SignaturePacket raise {
  make_document_signature_with(
    key,
    data,
    @packet.CanonicalTextDocument,
    hash,
    created?,
  )
}

///|
fn make_document_signature_with(
  key : PrivateKey,
  data : BytesView,
  sig_type : @packet.SigType,
  hash : @primitives.HashId,
  created? : Int32,
) -> @packet.SignaturePacket raise {
  let now = match created {
    Some(t) => t
    None => @primitives.unix_now().to_int()
  }
  let v6 = key.primary.public.version() >= 6
  let hashed : Array[@packet.SigSubpacket] = [
    creation_subpacket(now),
    key_flags_subpacket(0x02),
  ]
  if v6 {
    hashed.push(issuer_fingerprint_subpacket(key.primary.public))
  }
  let unhashed : Array[@packet.SigSubpacket] = if v6 {
    // §5.2.3.12: no Issuer Key ID subpacket on v6 signatures; the
    // fingerprint subpacket above carries the issuer.
    []
  } else {
    [
      {
        critical: false,
        sub_type: @packet.SIGSUB_ISSUER_KEY_ID,
        // No catch: a key whose id cannot be computed must fail the signing
        // loudly, not emit a signature with an empty issuer (F15).
        body: key.primary.public.key_id(),
      },
    ]
  }
  let salt : Bytes = if v6 {
    @primitives.random_bytes(@packet.salt_size_of_hash(hash.number()))
  } else {
    Bytes::new(0)
  }
  let sig : @packet.SignaturePacket = {
    version: if v6 {
      6
    } else {
      4
    },
    sig_type,
    pub_algo: key.primary.public.algo(),
    hash_algo: hash.number(),
    hashed_subpackets: hashed,
    unhashed_subpackets: unhashed,
    raw_hashed_area: Bytes::new(0),
    fast_hash: 0,
    salt,
    sig_params: [],
  }
  let digest = document_hash(sig, data, sig_type)
  finish_signature(sig, key.primary, hash, digest.exact_view())
}

///|
/// Signature type from the optional `text_mode` flag.
fn sig_type_of(text_mode : Bool?) -> @packet.SigType {
  if text_mode.unwrap_or(false) {
    @packet.CanonicalTextDocument
  } else {
    @packet.BinaryDocument
  }
}

///|
/// Default ceiling on decompressed payload size (M4): a 512 MiB bound on
/// what an untrusted compressed packet may expand to. Mirrors the Argon2
/// pre-allocation cap policy; override per call with
/// `max_decompressed~`.
pub const DEFAULT_MAX_DECOMPRESSED_BYTES : Int32 = 536870912

///|
/// Decompress a compressed packet payload (shared by the verification and
/// decryption walks, F16) under a hard output cap.
fn decompress_payload(
  comp : @packet.CompressedPacket,
  max_out : Int32,
) -> Bytes raise {
  match comp.algo {
    0 => comp.data
    1 => @compress.zip_decompress_capped(comp.data.exact_view(), max_out)
    2 => @compress.zlib_decompress_capped(comp.data.exact_view(), max_out)
    _ => @error.fail("api: unsupported compression algorithm \{comp.algo}")
  }
}

///|
/// Optionally wrap an inner packet stream in a Compressed packet (M4).
/// Only ZIP/ZLIB have codecs here — BZip2 is decode-only and
/// `Uncompressed` means the caller passed something odd.
fn maybe_compress_inner(
  inner : BytesView,
  compress? : @compress.CompressedAlgo,
) -> Bytes raise {
  match compress {
    None => inner.to_owned()
    Some(algo) =>
      match algo {
        // Body = algorithm octet || compressed stream (RFC 9580 §5.6).
        @compress.Zip =>
          @packet.serialize_packet(
            @packet.Compressed,
            Bytes::from_array([1])
            .add(@compress.zip_compress(inner))
            .exact_view(),
          )
        @compress.Zlib =>
          @packet.serialize_packet(
            @packet.Compressed,
            Bytes::from_array([2])
            .add(@compress.zlib_compress(inner))
            .exact_view(),
          )
        _ =>
          @error.fail(
            "api: unsupported output compression algorithm (BZip2 is decode-only)",
          )
      }
  }
}

///|
/// Maximum nested Compressed packet depth on any packet walk (S03). gpg
/// emits at most one layer; three matches `extract_literal`'s precedent.
/// Without the cap, an algo-0 Compressed packet costs ~3 wire octets per
/// layer, so a few MB of untrusted input can drive hundreds of thousands
/// of recursion levels.
const MAX_COMPRESSED_DEPTH : Int32 = 3

///|
/// Walk a packet stream collecting the literal data and the signature,
/// recursing into compressed packets (they carry either [Literal] or the
/// [OPS, Literal, Sig] gpg -es shape). Returns the accumulated
/// (data, sig, remaining budget); unknown tags (e.g. OnePassSig) are
/// skipped.
///
/// `budget` is the *cumulative* cap on decompressed output across every
/// nested layer (S03): each decompression's cap is whatever remains of the
/// budget, so N nested layers cannot expand to N x `max_decompressed`.
/// Algo-0 packets perform no expansion and consume no budget.
fn collect_data_sig(
  packets : Array[@packet.RawPacket],
  data0 : Bytes?,
  sig0 : @packet.SignaturePacket?,
  max_decompressed : Int32,
  budget : Int32,
  depth : Int32,
) -> (Bytes?, @packet.SignaturePacket?, Int32) raise {
  let mut data = data0
  let mut sig = sig0
  let mut remaining = budget
  for p in packets {
    match p.tag() {
      @packet.Literal =>
        data = Some(@packet.LiteralPacket::parse(p.body().exact_view()).data())
      @packet.Signature =>
        sig = Some(@packet.SignaturePacket::parse(p.body().exact_view()))
      @packet.Compressed => {
        guard depth < MAX_COMPRESSED_DEPTH else {
          @error.fail(
            "api: compressed packets nested more than \{MAX_COMPRESSED_DEPTH} levels deep",
          )
        }
        let comp = @packet.CompressedPacket::parse(p.body().exact_view())
        let inner : Bytes = match comp.algo {
          0 => comp.data
          _ => {
            let cap = if remaining < max_decompressed {
              remaining
            } else {
              max_decompressed
            }
            let out = decompress_payload(comp, cap)
            remaining = remaining - out.length()
            out
          }
        }
        let (d2, s2, rem2) = collect_data_sig(
          @packet.parse_packets(inner.exact_view()),
          data,
          sig,
          max_decompressed,
          remaining,
          depth + 1,
        )
        data = d2
        sig = s2
        remaining = rem2
      }
      _ => ()
    }
  }
  (data, sig, remaining)
}

///|
/// Combine the collected data/signature into the verification outcome.
fn finish_inline_verify(
  data : Bytes?,
  sig : @packet.SignaturePacket?,
  verifier : PublicKey?,
) -> (Bytes, VerifyStatus) raise {
  match (data, sig) {
    (Some(d), Some(s)) => {
      let status : VerifyStatus = match verifier {
        Some(v) => verify_signature_packet(s, d.exact_view(), v)
        None => NoVerifier
      }
      (d, status)
    }
    (Some(d), None) => (d, NotSigned)
    _ => @error.fail("api: message has no data packet")
  }
}

///|
/// Create a detached ASCII-armored signature over `data`.
///
/// `created` is the signature creation time in seconds since 1970; without it
/// the current time is used. Passing the time makes the signature
/// reproducible for RSA PKCS#1 v1.5 and Ed25519 keys, which sign
/// deterministically (for example for reproducible builds).
pub fn sign_detached(
  data : BytesView,
  key : PrivateKey,
  text_mode? : Bool,
  created? : Int32,
) -> String raise {
  let sig_type = sig_type_of(text_mode)
  let sig = make_document_signature(key, data, sig_type, created?)
  @armor.Signature.encode(
    @packet.serialize_packet(@packet.Signature, sig.to_body().exact_view()).exact_view(),
  )
}

///|
/// Verification outcome, mirroring gopenpgp's status codes
/// (OK / NOT_SIGNED / NO_VERIFIER / FAILED) plus the lifecycle outcomes
/// gopenpgp v3 folds into a rejected signature.
pub(all) enum VerifyStatus {
  /// Signature verified against the given key.
  Ok
  /// The message carried no signature at all.
  NotSigned
  /// The signature belongs to a key we cannot verify with.
  NoVerifier
  /// A matching key was present but verification failed.
  Failed
  /// The signature is cryptographically valid but the signing key (or the
  /// signing subkey) is revoked — never `Ok` (S02; gopenpgp v3 rejects
  /// signatures from revoked keys).
  KeyRevoked
  /// The signature is cryptographically valid but the signing key or the
  /// signature itself has expired.
  Expired
} derive(Eq, Debug)

///|
/// Verify a detached armored signature over `data` using `verifier`.
pub fn verify_detached(
  data : BytesView,
  armored_signature : String,
  verifier : PublicKey,
) -> VerifyStatus raise {
  let block = @armor.decode(armored_signature)
  guard block.armor_type() == @armor.Signature else {
    @error.fail("api: not a signature armor block")
  }
  // The armor payload is a packet sequence; find the signature packet.
  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
    }
  }
  match sig {
    Some(s) => verify_signature_packet(s, data, verifier)
    None => @error.fail("api: no signature packet in armor block")
  }
}

///|
/// Verify a signature packet over document data with a transferable key.
fn verify_signature_packet(
  sig : @packet.SignaturePacket,
  data : BytesView,
  verifier : PublicKey,
) -> VerifyStatus raise {
  // RFC 9580 §5.2.3.6 (S01): an unrecognized subpacket marked critical
  // puts the whole signature in error — before any cryptography runs.
  if sig.has_unknown_critical_subpacket() {
    return Failed
  }
  // Which key of the transferable key issued this signature? The issuer
  // key id is matched against the primary and EVERY bound subkey (N2);
  // `comp` carries the matched subkey's own binding/revocation into the
  // lifecycle gate.
  let issuer = sig.issuer_key_id()
  let primary_id = verifier.primary.key_id()
  let signer : (@packet.PublicKeyPacket, SubkeyCert?)? = match issuer {
    Some(id) =>
      if primary_id.equal(id) {
        Some((verifier.primary, None))
      } else {
        // Lenient on purpose (F15): a subkey whose id cannot be computed
        // merely drops out of the candidate match.
        let mut hit : (@packet.PublicKeyPacket, SubkeyCert?)? = None
        for c in verifier.subkeys {
          let sid = c.key.key_id() catch { _ => Bytes::new(0) }
          if sid.length() > 0 && sid.equal(id) {
            hit = Some((c.key, Some(c)))
            break
          }
        }
        hit
      }
    None => Some((verifier.primary, None))
  }
  match signer {
    None => NoVerifier
    Some((signer_pk, comp)) => {
      let digest = document_hash(sig, data, sig.sig_type())
      // RFC 9580 §5.2.4: a v6 verifier MUST reject the signature when the
      // left 16 bits of the digest do not match the stored hash tag.
      if sig.version() >= 6 {
        let tag = (digest[0].to_int() << 8) | digest[1].to_int()
        if tag != sig.fast_hash() {
          return Failed
        }
      }
      let hash_id = match @primitives.hash_id_of_number(sig.hash_algo()) {
        Some(h) => h
        None => return Failed
      }
      let verified = verify_sig_material(
        signer_pk,
        hash_id,
        digest.exact_view(),
        sig.sig_params(),
      ) catch {
        _ => false
      }
      if verified {
        lifecycle_status(sig, verifier, comp)
      } else {
        Failed
      }
    }
  }
}

///|
/// Lifecycle gate after a cryptographically valid signature (S02): a
/// signature from a revoked or expired key is not `Ok`. With `now = 0`
/// (clock-less targets) the expiry checks are skipped but revocation
/// signatures are still cryptographically validated — the same degradation
/// `is_revoked` itself documents.
fn lifecycle_status(
  sig : @packet.SignaturePacket,
  verifier : PublicKey,
  comp : SubkeyCert?,
) -> VerifyStatus {
  let now = @primitives.unix_now()
  if verifier.is_revoked(now) {
    return KeyRevoked
  }
  match comp {
    Some(c) => {
      if subkey_revoked_at(c.revocation, verifier.primary, c.key, now) {
        return KeyRevoked
      }
      // A subkey only exists as part of its certificate: an expired
      // primary leaves the subkey's signatures without a current
      // certification too (N7).
      if verifier.is_expired(now) {
        return Expired
      }
      if packet_expired_at(c.key, Some(c.binding), now) ||
        sig_expired_opt(Some(c.binding), now) {
        return Expired
      }
    }
    None => if verifier.is_expired(now) { return Expired }
  }
  if sig_expired_at(sig, now) {
    return Expired
  }
  Ok
}

///|
/// The One-Pass Signature packet matching `sig`: version 3 for a v4 key
/// and version 6 for a v6 key. RFC 9580 §5.4 requires a v6 OPS to carry
/// the same hash salt as its Signature packet and the 32-octet signing
/// key fingerprint (in place of the v3 8-octet key id).
fn one_pass_sig_for(
  key : PrivateKey,
  sig : @packet.SignaturePacket,
  hash : @primitives.HashId,
) -> @packet.OnePassSigPacket raise {
  let v6 = key.primary.public.version() >= 6
  {
    version: if v6 {
      6
    } else {
      3
    },
    sig_type: sig.sig_type(),
    hash_algo: hash.number(),
    pub_algo: key.primary.public.algo(),
    key_id: if v6 {
      Bytes::new(0)
    } else {
      key.primary.public.key_id()
    },
    salt: if v6 {
      sig.salt()
    } else {
      Bytes::new(0)
    },
    fingerprint: if v6 {
      key.primary.public.fingerprint()
    } else {
      Bytes::new(0)
    },
    nested: false,
  }
}

///|
/// Create an inline-signed, armored message: One-Pass-Signature ||
/// Literal || Signature.
///
/// `created` is the signature creation time in seconds since 1970; without it
/// the current time is used (see `sign_detached` — deterministic for
/// RSA/Ed25519 keys, so the message becomes reproducible). `file_name` names
/// the literal packet (N9; default "message.txt").
pub fn sign_inline(
  data : BytesView,
  key : PrivateKey,
  text_mode? : Bool,
  created? : Int32,
  file_name? : String,
) -> String raise {
  let sig_type = sig_type_of(text_mode)
  let sig = make_document_signature(key, data, sig_type, created?)
  let literal = @packet.LiteralPacket::binary(
    data,
    file_name=file_name.unwrap_or("message.txt"),
  )
  let ops = one_pass_sig_for(key, sig, @primitives.Sha256)
  let w = @packet.Writer::new()
  w.bytes(
    @packet.serialize_packet(@packet.OnePassSig, ops.to_body().exact_view()).exact_view(),
  )
  w.bytes(
    @packet.serialize_packet(@packet.Literal, literal.to_body().exact_view()).exact_view(),
  )
  w.bytes(
    @packet.serialize_packet(@packet.Signature, sig.to_body().exact_view()).exact_view(),
  )
  @armor.Message.encode(w.to_bytes().exact_view())
}

///|
/// Verify an inline-signed armored message; returns the literal data and
/// the verification status against `verifier`.
pub fn verify_inline(
  message : String,
  verifier : PublicKey,
  max_decompressed~ : Int32 = DEFAULT_MAX_DECOMPRESSED_BYTES,
) -> (Bytes, VerifyStatus) raise {
  let block = @armor.decode(message)
  let packets = @packet.parse_packets(block.data().exact_view())
  let (data, sig, _budget) = collect_data_sig(
    packets,
    None,
    None,
    max_decompressed,
    max_decompressed,
    0,
  )
  finish_inline_verify(data, sig, Some(verifier))
}

///|
/// Sign and encrypt in one step (inline signature inside SEIPD).
///
/// `created` is the signature creation time in seconds since 1970; without it
/// the current time is used (see `sign_detached`). `file_name` names the
/// literal packet (N9; default "message.txt").
pub fn sign_and_encrypt(
  data : BytesView,
  signer : PrivateKey,
  recipients : Array[PublicKey],
  text_mode? : Bool,
  compress? : @compress.CompressedAlgo,
  created? : Int32,
  file_name? : String,
) -> String raise {
  let sig_type = sig_type_of(text_mode)
  let sig = make_document_signature(signer, data, sig_type, created?)
  let literal = @packet.LiteralPacket::binary(
    data,
    file_name=file_name.unwrap_or("message.txt"),
  )
  let ops = one_pass_sig_for(signer, sig, @primitives.Sha256)
  let inner = @packet.Writer::new()
  inner.bytes(
    @packet.serialize_packet(@packet.OnePassSig, ops.to_body().exact_view()).exact_view(),
  )
  inner.bytes(
    @packet.serialize_packet(@packet.Literal, literal.to_body().exact_view()).exact_view(),
  )
  inner.bytes(
    @packet.serialize_packet(@packet.Signature, sig.to_body().exact_view()).exact_view(),
  )
  seal_to_recipients(
    maybe_compress_inner(inner.to_bytes().exact_view(), compress?).exact_view(),
    recipients,
  )
}

///|
/// Decrypt an armored message (password OR private keys) and verify an
/// inline signature against `verifier`. Returns (data, status).
pub fn decrypt_and_verify(
  message : String,
  decryption_keys : Array[PrivateKey],
  verifier : PublicKey?,
  max_decompressed~ : Int32 = DEFAULT_MAX_DECOMPRESSED_BYTES,
) -> (Bytes, VerifyStatus) raise {
  // Try private-key decryption first, then fall back to no-password
  // path only when no keys given? Keep explicit: caller decides.
  let block = @armor.decode(message)
  let data_bytes = @armor.ArmorBlock::data(block)
  let packets = @packet.parse_packets(data_bytes.exact_view())
  // Try each key in turn. `decrypt_inner_with_key` is the same matcher the
  // plain private-key path uses, so it covers v6 PKESKs (fingerprint) and
  // v3 PKESKs (key id) as well as SEIPD v1 and v2 — this pipeline must not
  // be able to reject a message the library itself can produce.
  let mut inner_opt : Bytes? = None
  for key in decryption_keys {
    match decrypt_inner_with_key(packets, key) {
      Some(b) => {
        inner_opt = Some(b)
        break
      }
      None => ()
    }
  }
  // Keep the raw inner packet stream (it may carry a one-pass signature
  // around the literal data).
  let inner : Bytes = match inner_opt {
    Some(b) => b
    None => @error.fail("api: no matching decryption key")
  }
  // Walk inner packets: OPS? literal | compressed; trailing sig.
  let inner_packets = @packet.parse_packets(inner.exact_view())
  let (data, sig, _budget) = collect_data_sig(
    inner_packets,
    None,
    None,
    max_decompressed,
    max_decompressed,
    0,
  )
  finish_inline_verify(data, sig, verifier)
}

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

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