///|
/// Stored idempotency decision.
pub(all) struct IdempotencyRecord {
  key : String
  status : HookStatus
  note : String
  first_seen_at : String
  expires_at : String
} derive(Eq, Debug)

///|
/// In-memory idempotency store for tests, examples, and single-process tools.
pub(all) struct InMemoryDeduper {
  records : Map[String, IdempotencyRecord]
} derive(Eq, Debug)

///|
/// Create an idempotency record.
pub fn IdempotencyRecord::IdempotencyRecord(
  key : StringView,
  status : HookStatus,
  note : StringView,
  first_seen_at? : StringView = "",
  expires_at? : StringView = "",
) -> IdempotencyRecord {
  {
    key: key.to_owned(),
    status,
    note: note.to_owned(),
    first_seen_at: first_seen_at.to_owned(),
    expires_at: expires_at.to_owned(),
  }
}

///|
/// Create an empty in-memory store.
pub fn new_in_memory_deduper() -> InMemoryDeduper {
  { records: Map([]) }
}

///|
/// Check if an idempotency key already exists.
pub fn InMemoryDeduper::contains(
  self : InMemoryDeduper,
  key : StringView,
) -> Bool {
  self.records.get(key.to_owned()) is Some(_)
}

///|
/// Read a record by key.
pub fn InMemoryDeduper::get(
  self : InMemoryDeduper,
  key : StringView,
) -> IdempotencyRecord? {
  self.records.get(key.to_owned())
}

///|
/// Store a record and return the updated deduper.
pub fn InMemoryDeduper::put(
  self : InMemoryDeduper,
  record : IdempotencyRecord,
) -> InMemoryDeduper {
  let records = self.records
  records[record.key] = record
  { records, }
}

///|
/// Store an accepted event.
pub fn InMemoryDeduper::remember_event(
  self : InMemoryDeduper,
  event : HookEvent,
  now : StringView,
) -> InMemoryDeduper {
  self.put(
    IdempotencyRecord(
      event.idempotency_key,
      Accepted,
      "accepted " + event.event_type,
      first_seen_at=now,
    ),
  )
}

///|
/// Determine whether an event should be processed.
pub fn InMemoryDeduper::should_process(
  self : InMemoryDeduper,
  event : HookEvent,
) -> Bool {
  !self.contains(event.idempotency_key)
}

///|
/// Return accepted when the event is new and ignored when duplicate.
pub fn InMemoryDeduper::guard_event(
  self : InMemoryDeduper,
  event : HookEvent,
) -> HookResult {
  if self.should_process(event) {
    accepted(message="new delivery " + event.idempotency_key)
  } else {
    ignored(message="duplicate delivery " + event.idempotency_key)
  }
}

///|
/// Count records.
pub fn InMemoryDeduper::size(self : InMemoryDeduper) -> Int {
  let mut count = 0
  for _ in self.records {
    count = count + 1
  }
  count
}