///|
/// In-memory append-only journal.
///
/// Persistence adapters can reconstruct a journal with `from_events` and then
/// call `validate` before allowing replay.
pub struct Journal[P] {
  records : Array[JournalEvent[P]]
}

///|
/// Creates an empty event journal.
pub fn[P] Journal::new() -> Journal[P] {
  { records: [] }
}

///|
/// Reconstructs a journal from externally loaded events.
///
/// The caller must validate the returned journal before replay.
pub fn[P] Journal::from_events(events : Array[JournalEvent[P]]) -> Journal[P] {
  { records: events.copy() }
}

///|
/// Returns the number of events in the journal.
pub fn[P] Journal::length(self : Journal[P]) -> Int {
  self.records.length()
}

///|
/// Returns an event by zero-based index.
pub fn[P] Journal::get(self : Journal[P], index : Int) -> JournalEvent[P]? {
  self.records.get(index)
}

///|
/// Returns a copy suitable for persistence or diagnostics.
pub fn[P] Journal::events(self : Journal[P]) -> Array[JournalEvent[P]] {
  self.records.copy()
}

///|
/// Returns a journal containing the first `count` events.
///
/// The returned journal preserves the original hash evidence and should still
/// be validated before replay.
pub fn[P] Journal::prefix(self : Journal[P], count : Int) -> Journal[P] {
  let events : Array[JournalEvent[P]] = []
  let limit = if count < 0 {
    0
  } else if count > self.records.length() {
    self.records.length()
  } else {
    count
  }
  for index = 0; index < limit; index = index + 1 {
    events.push(self.records[index])
  }
  Journal::from_events(events)
}

///|
/// Returns the hash-chain anchor at a sequence boundary.
pub fn[P] Journal::hash_at(self : Journal[P], sequence : Int) -> Int? {
  if sequence == 0 {
    Some(0)
  } else if sequence < 0 || sequence > self.records.length() {
    None
  } else {
    Some(self.records[sequence - 1].hash)
  }
}

///|
/// Returns the current hash-chain tail, or zero for an empty journal.
pub fn[P] Journal::tail_hash(self : Journal[P]) -> Int {
  if self.records.length() == 0 {
    0
  } else {
    self.records[self.records.length() - 1].hash
  }
}

///|
/// Appends one event and returns its complete journal record.
pub fn[P] Journal::append(
  self : Journal[P],
  kind : String,
  payload : P,
  correlation_id : String,
  payload_fingerprint : (P) -> String,
) -> JournalEvent[P] {
  let sequence = self.records.length() + 1
  let previous_hash = self.tail_hash()
  let hash = event_fingerprint(
    previous_hash,
    sequence,
    kind,
    correlation_id,
    payload_fingerprint(payload),
  )
  let event = { sequence, kind, payload, correlation_id, previous_hash, hash }
  self.records.push(event)
  event
}

///|
/// Validates sequence continuity and the complete event hash chain.
pub fn[P] Journal::validate(
  self : Journal[P],
  payload_fingerprint : (P) -> String,
) -> ChainValidation {
  let mut previous_hash = 0
  for index = 0; index < self.records.length(); index = index + 1 {
    let event = self.records[index]
    let expected_sequence = index + 1
    if event.sequence != expected_sequence {
      return Invalid(
        UnexpectedSequence(index, expected_sequence, event.sequence),
      )
    }
    if event.previous_hash != previous_hash {
      return Invalid(
        BrokenPreviousHash(event.sequence, previous_hash, event.previous_hash),
      )
    }
    let expected_hash = event_fingerprint(
      previous_hash,
      event.sequence,
      event.kind,
      event.correlation_id,
      payload_fingerprint(event.payload),
    )
    if event.hash != expected_hash {
      return Invalid(
        InvalidEventHash(event.sequence, expected_hash, event.hash),
      )
    }
    previous_hash = event.hash
  }
  Valid(self.records.length(), previous_hash)
}