// ---------------------------------------------------------------------------
// RecordingSessionStore — load/save history with metadata snapshots.
// ---------------------------------------------------------------------------

///|
/// A recorded session operation. Constructors are prefixed with `Op` to avoid
/// ambiguity with `SessionError::Load`/`SessionError::Save`.
pub(all) enum SessionOp {
  OpLoad(id~ : String, result~ : Result[@types.Session, @error.SessionError])
  OpSave(
    id~ : String,
    metadata~ : Map[String, Json],
    result~ : Result[Unit, @error.SessionError]
  )
  OpAppend(
    id~ : String,
    from_index~ : Int,
    messages~ : Array[@kernel.Message],
    result~ : Result[Unit, @error.SessionError]
  )
  OpTruncate(
    id~ : String,
    from_index~ : Int,
    result~ : Result[Unit, @error.SessionError]
  )
} derive(Debug)

///|
/// SessionStore fake that keeps an in-memory map and records every load/save
/// with the metadata snapshot at save time. Load/save failures are
/// configurable per id.
pub(all) struct RecordingSessionStore {
  store : Map[String, @types.Session]
  mut load_failures : Map[String, @error.SessionError]
  mut save_failures : Array[String]
  mut ops : Array[SessionOp]
}

///|
pub fn RecordingSessionStore::RecordingSessionStore() -> RecordingSessionStore {
  {
    store: Map::from_array([]),
    load_failures: Map::from_array([]),
    save_failures: [],
    ops: [],
  }
}

///|
/// Seed a session id with an existing session (including metadata).
pub fn RecordingSessionStore::seed(
  self : RecordingSessionStore,
  id : String,
  session : @types.Session,
) -> Unit {
  self.store[id] = snapshot_session(session.messages, session.metadata)
}

///|
/// Configure load to fail for a given id.
pub fn RecordingSessionStore::fail_load(
  self : RecordingSessionStore,
  id : String,
  err : @error.SessionError,
) -> Unit {
  self.load_failures[id] = err
}

///|
/// Configure save to fail for a given id (all subsequent saves to that id).
pub fn RecordingSessionStore::fail_save(
  self : RecordingSessionStore,
  id : String,
) -> Unit {
  self.save_failures.push(id)
}

///|
/// Recorded operations in order.
pub fn RecordingSessionStore::ops(
  self : RecordingSessionStore,
) -> Array[SessionOp] {
  self.ops.map(fn(op) {
    match op {
      OpLoad(id~, result~) =>
        OpLoad(
          id~,
          result=match result {
            Ok(session) =>
              Ok(snapshot_session(session.messages, session.metadata))
            Err(error) => Err(error)
          },
        )
      OpSave(id~, metadata~, result~) =>
        OpSave(id~, metadata=snapshot_metadata(metadata), result~)
      OpAppend(id~, from_index~, messages~, result~) =>
        OpAppend(
          id~,
          from_index~,
          messages=snapshot_messages(messages),
          result~,
        )
      OpTruncate(id~, from_index~, result~) =>
        OpTruncate(id~, from_index~, result~)
    }
  })
}

///|
/// All save operations, in order, as (id, metadata_snapshot) pairs.
pub fn RecordingSessionStore::save_snapshots(
  self : RecordingSessionStore,
) -> Array[(String, Map[String, Json])] {
  let out : Array[(String, Map[String, Json])] = []
  for op in self.ops {
    match op {
      OpSave(id~, metadata~, ..) => out.push((id, snapshot_metadata(metadata)))
      _ => ()
    }
  }
  out
}

///|
/// The configured save failure for `id`, if one was armed via `fail_save`.
/// Shared guard of every mutating store operation.
fn RecordingSessionStore::guard_save(
  self : RecordingSessionStore,
  id : String,
) -> @error.SessionError? {
  if self.save_failures.contains(id) {
    Some(@error.SessionError::Save("configured save failure for '\{id}'"))
  } else {
    None
  }
}

///|
/// Record an operation, then surface its failure: ops keep arrival order and
/// a configured error raises only after being recorded. Shared tail of
/// load/save/append/truncate.
fn[T] RecordingSessionStore::record(
  self : RecordingSessionStore,
  op : SessionOp,
  result : Result[T, @error.SessionError],
) -> T raise @error.SessionError {
  self.ops.push(op)
  match result {
    Ok(value) => value
    Err(e) => raise e
  }
}

///|
pub impl @port.SessionStore for RecordingSessionStore with fn load(
  self,
  id : String,
) -> @types.Session raise @error.SessionError {
  let result : Result[@types.Session, @error.SessionError] = match
    self.load_failures.get(id) {
    Some(e) => Err(e)
    None =>
      match self.store.get(id) {
        Some(s) => Ok(snapshot_session(s.messages, s.metadata))
        None => Ok({ messages: [], metadata: Map::from_array([]), })
      }
  }
  let recorded_result = match result {
    Ok(session) => Ok(snapshot_session(session.messages, session.metadata))
    Err(error) => Err(error)
  }
  self.record(OpLoad(id~, result=recorded_result), result)
}

///|
pub impl @port.SessionStore for RecordingSessionStore with fn save(
  self,
  id : String,
  session : @types.Session,
) -> Unit raise @error.SessionError {
  let meta_copy = snapshot_metadata(session.metadata)
  let result : Result[Unit, @error.SessionError] = match self.guard_save(id) {
    Some(e) => Err(e)
    None => {
      self.store[id] = snapshot_session(session.messages, session.metadata)
      Ok(())
    }
  }
  self.record(OpSave(id~, metadata=meta_copy, result~), result)
}

///|
pub impl @port.SessionStore for RecordingSessionStore with fn append_messages(
  self,
  id : String,
  from_index : Int,
  messages : ArrayView[@kernel.Message],
) -> Unit raise @error.SessionError {
  let msg_copy = snapshot_messages(messages.to_owned())
  let result : Result[Unit, @error.SessionError] = match self.guard_save(id) {
    Some(e) => Err(e)
    None => {
      let session = match self.store.get(id) {
        Some(s) => snapshot_session(s.messages, s.metadata)
        None => { messages: [], metadata: Map::from_array([]), }
      }
      let merged = session.messages.copy()
      for msg in msg_copy {
        merged.push(msg)
      }
      self.store[id] = { messages: merged, metadata: session.metadata, }
      Ok(())
    }
  }
  self.record(OpAppend(id~, from_index~, messages=msg_copy, result~), result)
}

///|
pub impl @port.SessionStore for RecordingSessionStore with fn truncate(
  self,
  id : String,
  from_index : Int,
) -> Unit raise @error.SessionError {
  let result : Result[Unit, @error.SessionError] = match self.guard_save(id) {
    Some(e) => Err(e)
    None => {
      let session = match self.store.get(id) {
        Some(s) => snapshot_session(s.messages, s.metadata)
        None => { messages: [], metadata: Map::from_array([]), }
      }
      let len = session.messages.length()
      let keep = if from_index < 0 {
        0
      } else if from_index > len {
        len
      } else {
        from_index
      }
      let kept = session.messages[:keep].to_owned()
      self.store[id] = { messages: kept, metadata: session.metadata, }
      Ok(())
    }
  }
  self.record(OpTruncate(id~, from_index~, result~), result)
}