///|
/// Errors raised while managing local durable state.
pub(all) suberror StoreError {
  InvalidCapacity
  QueueFull
  EventNotFound(Int32)
  InvalidCheckpoint
  SequenceExhausted
} derive(Debug, Eq)

///|
/// The policy applied when the pending journal reaches its capacity.
pub(all) enum QueueFullPolicy {
  RejectNew
  DropOldest
} derive(Debug, Eq, ToJson, FromJson)

///|
/// Capacity and overload behavior for a local event journal.
pub(all) struct JournalConfig {
  capacity : Int32
  full_policy : QueueFullPolicy
} derive(Debug, Eq, ToJson, FromJson)

///|
/// Creates a journal configuration with a positive pending capacity.
pub fn JournalConfig::new(
  capacity~ : Int32,
  full_policy~ : QueueFullPolicy,
) -> JournalConfig raise StoreError {
  if capacity <= 0 {
    raise StoreError::InvalidCapacity
  }
  { capacity, full_policy, }
}

///|
/// Retention state for one journal entry.
pub(all) enum JournalStatus {
  Pending
  Acknowledged
} derive(Debug, Eq, ToJson, FromJson)

///|
/// One ordered event retained for local replay or audit.
pub(all) struct JournalEntry {
  sequence : Int32
  envelope : MessageEnvelope
  mut status : JournalStatus
  mut attempts : Int32
} derive(Debug, Eq, ToJson, FromJson)

///|
/// Reports whether an entry still awaits acknowledgement.
pub fn JournalEntry::is_pending(self : JournalEntry) -> Bool {
  self.status == JournalStatus::Pending
}

///|
/// A recoverable snapshot of the node's local operational state.
pub(all) struct LocalStateSnapshot {
  node : Node
  config : ConfigSnapshot?
  saved_at : Timestamp
} derive(Debug, Eq, ToJson, FromJson)

///|
/// The persistence boundary used to recover a local store after restart.
pub(all) struct StoreCheckpoint {
  config : JournalConfig
  state : LocalStateSnapshot?
  journal : Array[JournalEntry]
  next_sequence : Int32
  dropped_count : Int32
} derive(Debug, Eq, ToJson, FromJson)

///|
/// Mutable local state and event journal for one edge node process.
pub(all) struct LocalStore {
  config : JournalConfig
  mut state : LocalStateSnapshot?
  mut journal : Array[JournalEntry]
  mut next_sequence : Int32
  mut dropped_count : Int32
}

///|
/// Creates an empty local store.
pub fn LocalStore::new(config~ : JournalConfig) -> LocalStore {
  { config, state: None, journal: [], next_sequence: 1, dropped_count: 0, }
}

///|
/// Restores a local store from a previously captured checkpoint.
pub fn LocalStore::restore(
  checkpoint~ : StoreCheckpoint,
) -> LocalStore raise StoreError {
  let config = JournalConfig::new(
    capacity=checkpoint.config.capacity,
    full_policy=checkpoint.config.full_policy,
  )
  if checkpoint.next_sequence <= 0 ||
    checkpoint.dropped_count < 0 ||
    checkpoint.journal.length() > 100000 {
    raise StoreError::InvalidCheckpoint
  }
  match checkpoint.state {
    Some(snapshot) => {
      if snapshot.node.id.value == "" ||
        snapshot.node.created_at.unix_seconds < 0 ||
        snapshot.node.updated_at.unix_seconds <
        snapshot.node.created_at.unix_seconds ||
        snapshot.saved_at.unix_seconds < snapshot.node.updated_at.unix_seconds {
        raise StoreError::InvalidCheckpoint
      }
      match snapshot.config {
        Some(saved_config) =>
          if saved_config.version.value <= 0 ||
            saved_config.content == "" ||
            saved_config.created_at.unix_seconds < 0 {
            raise StoreError::InvalidCheckpoint
          }
        None => ()
      }
    }
    None => ()
  }
  let pending = pending_entry_count(checkpoint.journal)
  if pending > config.capacity {
    raise StoreError::InvalidCheckpoint
  }
  let mut previous_sequence = 0
  for i in 0..= checkpoint.next_sequence ||
      entry.attempts < 0 {
      raise StoreError::InvalidCheckpoint
    }
    previous_sequence = entry.sequence
    if entry.envelope.node_id.value == "" ||
      entry.envelope.created_at.unix_seconds < 0 {
      raise StoreError::InvalidCheckpoint
    }
    let _ = MessageEnvelope::new(
      version=entry.envelope.version,
      message_id=entry.envelope.message_id,
      node_id=entry.envelope.node_id,
      created_at=entry.envelope.created_at,
      correlation_id=entry.envelope.correlation_id,
      payload=entry.envelope.payload,
    ) catch {
      _ => raise StoreError::InvalidCheckpoint
    }
  }
  {
    config,
    state: checkpoint.state,
    journal: copy_journal(checkpoint.journal),
    next_sequence: checkpoint.next_sequence,
    dropped_count: checkpoint.dropped_count,
  }
}

///|
/// Replaces the recoverable node and configuration snapshot.
pub fn LocalStore::save_state(
  self : LocalStore,
  snapshot~ : LocalStateSnapshot,
) -> Unit {
  self.state = Some(snapshot)
}

///|
/// Returns the latest local state snapshot, if one has been saved.
pub fn LocalStore::state(self : LocalStore) -> LocalStateSnapshot? {
  self.state
}

///|
/// Appends an event and returns its local journal sequence.
pub fn LocalStore::append_event(
  self : LocalStore,
  envelope~ : MessageEnvelope,
) -> Int32 raise StoreError {
  if self.next_sequence >= 2147483647 {
    raise StoreError::SequenceExhausted
  }
  if self.pending_count() >= self.config.capacity {
    match self.config.full_policy {
      QueueFullPolicy::RejectNew => raise StoreError::QueueFull
      QueueFullPolicy::DropOldest => self.drop_oldest_pending()
    }
  }
  let sequence = self.next_sequence
  self.next_sequence = sequence + 1
  self.journal.push({
    sequence,
    envelope,
    status: JournalStatus::Pending,
    attempts: 0,
  })
  sequence
}

///|
/// Returns all entries that still await acknowledgement.
pub fn LocalStore::pending_events(self : LocalStore) -> Array[JournalEntry] {
  self.journal.filter(entry => entry.status == JournalStatus::Pending)
}

///|
/// Returns the complete retained journal in sequence order.
pub fn LocalStore::journal_entries(self : LocalStore) -> Array[JournalEntry] {
  copy_journal(self.journal)
}

///|
/// Returns the number of events awaiting acknowledgement.
pub fn LocalStore::pending_count(self : LocalStore) -> Int32 {
  pending_entry_count(self.journal)
}

///|
/// Returns how many pending events have been dropped by overload policy.
pub fn LocalStore::dropped_count(self : LocalStore) -> Int32 {
  self.dropped_count
}

///|
/// Records one replay attempt for a pending event.
pub fn LocalStore::record_attempt(
  self : LocalStore,
  sequence~ : Int32,
) -> Unit raise StoreError {
  for index in 0..= 2147483647 {
          raise StoreError::InvalidCheckpoint
        }
        self.journal[index].attempts = self.journal[index].attempts + 1
      }
      return
    }
  }
  raise StoreError::EventNotFound(sequence)
}

///|
/// Marks an event acknowledged; repeating the acknowledgement is harmless.
pub fn LocalStore::acknowledge(
  self : LocalStore,
  sequence~ : Int32,
) -> Unit raise StoreError {
  for index in 0.. StoreCheckpoint {
  {
    config: self.config,
    state: self.state,
    journal: copy_journal(self.journal),
    next_sequence: self.next_sequence,
    dropped_count: self.dropped_count,
  }
}

///|
fn LocalStore::drop_oldest_pending(self : LocalStore) -> Unit raise StoreError {
  let mut drop_index : Int32? = None
  for index in 0.. drop_index = Some(index)
        Some(_) => ()
      }
    }
  }
  match drop_index {
    Some(index) => {
      if self.dropped_count >= 2147483647 {
        raise StoreError::InvalidCheckpoint
      }
      let retained : Array[JournalEntry] = []
      for i in 0.. raise StoreError::InvalidCheckpoint
  }
}

///|
fn pending_entry_count(entries : Array[JournalEntry]) -> Int32 {
  let mut count = 0
  for entry in entries {
    if entry.status == JournalStatus::Pending {
      count = count + 1
    }
  }
  count
}

///|
fn copy_journal(entries : Array[JournalEntry]) -> Array[JournalEntry] {
  let copy : Array[JournalEntry] = []
  for entry in entries {
    copy.push({
      sequence: entry.sequence,
      envelope: entry.envelope,
      status: entry.status,
      attempts: entry.attempts,
    })
  }
  copy
}