///|
priv suberror ReplayError {
  ReplayError(String)
}

///|
fn json_copy(value : Json) -> Json {
  match value {
    Array(items) => Json::array(items.map(json_copy))
    Object(fields) => {
      let cloned : Map[String, Json] = Map([])
      for key, item in fields {
        cloned[key] = json_copy(item)
      }
      Json::object(cloned)
    }
    _ => value
  }
}

///|
fn[T] require(result : Result[T, String]) -> T raise ReplayError {
  match result {
    Ok(v) => v
    Err(message) => raise ReplayError(message)
  }
}

///|
fn field(value : Json, key : String) -> Json raise ReplayError {
  guard value is Object(fields) else { raise ReplayError("expected an object") }
  match fields.get(key) {
    Some(v) => v
    None => raise ReplayError("missing field: " + key)
  }
}

///|
fn optional(value : Json, key : String, fallback : Json) -> Json {
  match value {
    Object(fields) => fields.get(key).unwrap_or(fallback)
    _ => fallback
  }
}

///|
fn string_field(value : Json, key : String) -> String raise ReplayError {
  guard field(value, key) is String(text) else {
    raise ReplayError("expected string: " + key)
  }
  text
}

///|
fn apply_command(h : History[Json], command : Json) -> Unit raise {
  let name = string_field(command, "op")
  match name {
    "record" => {
      let label : String = @json.from_json(optional(command, "label", "Edit"))
      let group : String = @json.from_json(optional(command, "group", ""))
      ignore(require(h.record(field(command, "value"), label, group~)))
    }
    "undo" => ignore(require(h.undo()))
    "redo" => ignore(require(h.redo()))
    "jump" =>
      ignore(require(h.jump_to(require(exact_int(field(command, "index"))))))
    "begin" => require(h.begin(string_field(command, "label")))
    "commit" => ignore(require(h.commit()))
    "rollback" => ignore(require(h.rollback()))
    "save" => require(h.mark_saved())
    "forget_saved" => h.forget_saved()
    "break_group" => h.break_group()
    "clear" => require(h.clear())
    "capacity" =>
      require(h.set_capacity(require(exact_int(field(command, "limit")))))
    "reset" => require(h.reset(field(command, "value")))
    _ => raise ReplayError("unknown operation: " + name)
  }
}

///|
/// Portable JSON adapter for CLI/JS. Replays at most 2000 commands atomically.
/// Errors produce no output session; callers keep their previous document.
pub fn replay(text : String) -> String {
  try {
    let request = parse_request(text)
    let h = history_from_request(request)
    guard field(request, "commands") is Array(commands) else {
      raise ReplayError("commands must be an array")
    }
    if commands.length() > 2000 {
      raise ReplayError("at most 2000 commands per request")
    }
    for command in commands {
      apply_command(h, command)
    }
    status_json(h).stringify()
  } catch {
    error => {
      let response : Json = { "ok": false, "error": error_message(error) }
      response.stringify()
    }
  }
}

///|
fn error_message(error : Error) -> String {
  match error {
    ReplayError(message) => message
    @json.JsonDecodeError((path, message)) => path.to_string() + ": " + message
    _ => error.to_string()
  }
}

///|
fn validate_json(value : Json, depth : Int) -> Unit raise ReplayError {
  if depth > 64 {
    raise ReplayError("JSON nesting exceeds 64")
  }
  match value {
    Number(n, ..) =>
      if n.is_nan() || n < -9007199254740991.0 || n > 9007199254740991.0 {
        raise ReplayError(
          "JSON bridge numbers must be finite and within the safe integer range",
        )
      }
    Array(items) =>
      for item in items {
        validate_json(item, depth + 1)
      }
    Object(fields) =>
      for _, item in fields {
        validate_json(item, depth + 1)
      }
    _ => ()
  }
}

///|
fn history_from_request(request : Json) -> History[Json] raise {
  match optional(request, "session", Json::null()) {
    Null => {
      let limit = require(exact_int(optional(request, "limit", 100)))
      require(
        History::new(
          field(request, "initial"),
          copy=json_copy,
          equal=fn(a, b) { a == b },
          limit~,
        ),
      )
    }
    session =>
      require(
        restore_session(session, fn(v) { Ok(json_copy(v)) }, copy=json_copy, equal=fn(
          a,
          b,
        ) {
          a == b
        }),
      )
  }
}

///|
fn status_json(h : History[Json], include_session? : Bool = true) -> Json {
  let response : Json = {
    "ok": true,
    "state": h.state(),
    "dirty": h.is_dirty(),
    "can_undo": h.can_undo(),
    "can_redo": h.can_redo(),
    "undo_label": match h.undo_label() {
      Some(label) => label.to_json()
      None => Json::null()
    },
    "redo_label": match h.redo_label() {
      Some(label) => label.to_json()
      None => Json::null()
    },
    "undo_depth": h.undo_depth(),
    "redo_depth": h.redo_depth(),
    "transaction_depth": h.transaction_depth(),
    "capacity": h.capacity(),
    "revision_id": h.revision_id(),
    "timeline": h.timeline(),
  }
  if include_session && response is Object(fields) {
    fields["session"] = match h.export_session(fn(v) { v }) {
      Ok(v) => v
      Err(_) => Json::null()
    }
  }
  response
}