// nonce_store.mbt — Replay-protection nonce store.
//
// A nonce binds a signature to a single use: once `(keyid, nonce)` has been
// recorded, a second presentation is rejected as `DuplicateNonce`. The
// in-memory store is for single-process use (tests, examples); production
// deployments must use a shared store (Redis, database) as documented in
// docs/security.md.

///|
/// The nonce store interface.
pub(open) trait NonceStore {
  /// Records `nonce` for `keyid` unless it was already seen, optionally
  /// honoring an `expires` time. Returns `Ok(())` on first use and
  /// `DuplicateNonce` on replay.
  fn check_and_store(Self, keyid : String, nonce : String, expires : Int64?) -> Result[
    Unit,
    HsError,
  ]
}

///|
/// A recorded nonce with an optional expiration.
struct NonceEntry {
  keyid : String
  nonce : String
  expires : Int64?
}

///|
/// An in-memory nonce store. Not safe for multi-process use.
pub struct InMemoryNonceStore {
  entries : Array[NonceEntry]
}

///|
/// Constructs an empty in-memory nonce store.
pub fn InMemoryNonceStore::new() -> InMemoryNonceStore {
  { entries: Array::new() }
}

///|
/// Removes entries whose `expires` time is in the past (relative to `now`).
/// Returns the number of entries removed.
pub fn InMemoryNonceStore::prune(self : InMemoryNonceStore, now : Int64) -> Int {
  let mut removed = 0
  let mut i = 0
  while i < self.entries.length() {
    let e = self.entries[i]
    if e.expires is Some(t) && t < now {
      let _ = self.entries.remove(i)
      removed = removed + 1
    } else {
      i = i + 1
    }
  }
  removed
}

///|
/// Returns the number of stored nonces.
pub fn InMemoryNonceStore::length(self : InMemoryNonceStore) -> Int {
  self.entries.length()
}

///|
/// Implements `NonceStore` for `InMemoryNonceStore`.
pub impl NonceStore for InMemoryNonceStore with fn check_and_store(
  self,
  keyid,
  nonce,
  expires,
) {
  for e in self.entries {
    if e.keyid == keyid && e.nonce == nonce {
      return Err(
        hs_error(
          ReplayProtection,
          DuplicateNonce,
          "nonce already used: " + nonce,
        ),
      )
    }
  }
  self.entries.push({ keyid, nonce, expires })
  Ok(())
}