///|
priv struct StoredRevision {
  id : Int
  label : String
  value : Json
} derive(ToJson, @json.FromJson)

///|
priv struct Session {
  version : Int
  limit : Int
  cursor : Int
  next_id : Int
  saved_id : Int?
  revisions : Array[StoredRevision]
} derive(ToJson, @json.FromJson)

///|
/// The codec owns the schema of T. Active previews cannot be persisted.
pub fn[T] History::export_session(
  self : History[T],
  encode : (T) -> Json,
) -> Result[Json, String] {
  if self.transaction_depth() != 0 {
    return Err("commit or rollback before export")
  }
  let session : Session = {
    version: 1,
    limit: self.limit,
    cursor: self.cursor,
    next_id: self.next_id,
    saved_id: self.saved_id,
    revisions: self.revisions.map(fn(r) {
      { id: r.id, label: r.label, value: encode((self.copy)(r.value)), }
    }),
  }
  Ok(ToJson::to_json(session))
}

///|
/// Restore validates metadata before constructing a new history. Edit groups close.
pub fn[T] restore_session(
  value : Json,
  decode : (Json) -> Result[T, String],
  copy~ : (T) -> T,
  equal~ : (T, T) -> Bool,
) -> Result[History[T], String] {
  match validate_session_metadata(value) {
    Err(message) => return Err(message)
    Ok(_) => ()
  }
  let session : Session = @json.from_json(value) catch {
    _ => return Err("invalid session field types")
  }
  let revisions : Array[Revision[T]] = []
  for stored in session.revisions {
    let decoded = match decode(stored.value) {
      Ok(v) => v
      Err(message) => return Err("invalid revision value: " + message)
    }
    revisions.push({
      value: copy(decoded),
      label: stored.label,
      group: "",
      id: stored.id,
    })
  }
  Ok({
    copy,
    same: equal,
    revisions,
    cursor: session.cursor,
    live: copy(revisions[session.cursor].value),
    merge_allowed: false,
    saved_id: session.saved_id,
    next_id: session.next_id,
    limit: session.limit,
    transactions: [],
  })
}

///|
fn exact_int(value : Json) -> Result[Int, String] {
  guard value is Number(n, ..) else { return Err("expected integer") }
  if n.is_nan() || n < -2147483648.0 || n > 2147483647.0 {
    return Err("integer out of range")
  }
  let i = n.to_int()
  if i.to_double() != n {
    return Err("fractional integer")
  }
  Ok(i)
}

///|
// Check all metadata before allocating typed revisions or invoking user codecs.
fn validate_session_metadata(value : Json) -> Result[Unit, String] {
  guard value is Object(fields) else { return Err("session must be an object") }
  for key in ["version", "limit", "cursor", "next_id"] {
    guard fields.get(key) is Some(number) && exact_int(number) is Ok(_) else {
      return Err("session metadata requires exact 32-bit integers")
    }
  }
  let version = exact_int(fields["version"]).unwrap()
  let limit = exact_int(fields["limit"]).unwrap()
  let cursor = exact_int(fields["cursor"]).unwrap()
  let next_id = exact_int(fields["next_id"]).unwrap()
  if version != 1 {
    return Err("unsupported session version")
  }
  if limit < 1 || limit > 10000 {
    return Err("invalid session capacity")
  }
  if next_id < 1 {
    return Err("invalid next revision identifier")
  }
  if fields.get("saved_id") is Some(number) && number != Json::null() {
    let id = match exact_int(number) {
      Ok(id) => id
      Err(_) => return Err("invalid saved revision identifier")
    }
    if id < 0 || id >= next_id {
      return Err("invalid saved revision identifier")
    }
  }
  guard fields.get("revisions") is Some(Array(items)) else {
    return Err("revisions must be an array")
  }
  if items.is_empty() || items.length() > limit + 1 {
    return Err("invalid revision count")
  }
  if cursor < 0 || cursor >= items.length() {
    return Err("invalid session cursor")
  }
  let mut previous = -1
  for item in items {
    guard item is Object(entry) && entry.get("id") is Some(number) else {
      return Err("invalid revision metadata")
    }
    let id = match exact_int(number) {
      Ok(id) => id
      Err(_) => return Err("revision id requires an exact integer")
    }
    if id <= previous || id >= next_id {
      return Err("revision identifiers must be increasing and below next_id")
    }
    if !(entry.get("label") is Some(String(_))) || !entry.contains("value") {
      return Err("invalid revision fields")
    }
    previous = id
  }
  Ok(())
}