///|
/// Incremental JSON adapter. Owns a history, not a growing replay script.
pub struct JsonEditor {
  priv history : History[Json]
  priv include_session : Bool
}

///|
/// Accept {initial, limit?} or {session}; optional include_session defaults to true.
/// Construction never executes commands.
pub fn JsonEditor::new(text : String) -> Result[JsonEditor, String] {
  try {
    let request = parse_request(text)
    if request is Object(fields) && fields.contains("commands") {
      return Err("JsonEditor construction does not accept replay commands")
    }
    let include_session : Bool = @json.from_json(
      optional(request, "include_session", true),
    )
    Ok({ history: history_from_request(request), include_session, })
  } catch {
    error => Err(error_message(error))
  }
}

///|
/// Return isolated state and metadata, optionally including the committed session.
pub fn JsonEditor::status(self : JsonEditor) -> String {
  status_json(self.history, include_session=self.include_session).stringify()
}

///|
/// Execute one command. A rejected command leaves the history unchanged.
/// prepare_save exports a clean candidate without marking the live state saved;
/// the host persists it, then sends save only after successful synchronous I/O.
pub fn JsonEditor::dispatch(self : JsonEditor, text : String) -> String {
  try {
    let command = parse_request(text)
    let op = string_field(command, "op")
    if op == "prepare_save" || op == "export" {
      let session = require(self.history.export_session(fn(v) { v }))
      if op == "prepare_save" && session is Object(fields) {
        fields["saved_id"] = self.history.revision_id().to_json()
      }
      // Validate the final candidate, including the new savepoint's width.
      let request : Json = if self.include_session {
        { "session": session }
      } else {
        { "session": session, "include_session": false }
      }
      ignore(parse_request(request.stringify()))
      let response : Json = { "ok": true, "session": session }
      return response.stringify()
    }
    if op != "status" {
      apply_command(self.history, command)
    }
    self.status()
  } catch {
    error => {
      let response : Json = { "ok": false, "error": error_message(error) }
      response.stringify()
    }
  }
}

///|
fn parse_request(text : String) -> Json raise {
  if text.length() > 2000000 {
    raise ReplayError("request exceeds 2 million UTF-16 units")
  }
  let request = @json.parse(text)
  validate_json(request, 0)
  request
}