///|
/// A JSON-RPC 2.0 error object.
pub(all) struct RpcError {
  code : Int32
  message : String
  data : Json?
} derive(Debug)

///|
/// One decoded JSON-RPC message from a server.
pub(all) enum RpcMessage {
  /// The response to a request mcpbox sent.
  Response(Int32, Json?, RpcError?)
  /// A server-initiated request. mcpbox answers unknown methods politely
  /// rather than leaving the server waiting. The id is kept as raw JSON
  /// because the spec allows string ids for server requests.
  ServerRequest(Json, String, Json?)
  /// A server notification, such as a log line or `tools/list_changed`.
  Notification(String, Json?)
} derive(Debug)

///|
/// Build a JSON-RPC 2.0 request object. The `repr` keeps ids integral in the
/// encoded form instead of printing them as `1.0`.
pub fn rpc_request(id : Int32, method_name : String, params : Json?) -> Json {
  let fields : Map[String, Json] = {
    "jsonrpc": Json::string("2.0"),
    "id": Json::number(id.to_double(), repr=id.to_string()),
    "method": Json::string(method_name),
  }
  match params {
    Some(p) => fields["params"] = p
    None => ()
  }
  Json::object(fields)
}

///|
/// Build a JSON-RPC 2.0 notification: a request with no id, which the server
/// must not answer.
pub fn rpc_notification(method_name : String, params : Json?) -> Json {
  let fields : Map[String, Json] = {
    "jsonrpc": Json::string("2.0"),
    "method": Json::string(method_name),
  }
  match params {
    Some(p) => fields["params"] = p
    None => ()
  }
  Json::object(fields)
}

///|
/// Build the response to a server-initiated request, echoing its id.
pub fn rpc_result_response(id : Json, result : Json) -> Json {
  Json::object({ "jsonrpc": Json::string("2.0"), "id": id, "result": result })
}

///|
/// Build a JSON-RPC error response, echoing the request's id.
pub fn rpc_error_response(id : Json, code : Int32, message : String) -> Json {
  Json::object({
    "jsonrpc": Json::string("2.0"),
    "id": id,
    "error": Json::object({
      "code": Json::number(code.to_double(), repr=code.to_string()),
      "message": Json::string(message),
    }),
  })
}

///|
/// Decode one JSON-RPC message. Returns `None` for a well-formed object that
/// is neither a response nor a notification mcpbox cares about, so the caller
/// can simply skip it.
pub fn decode_rpc_message(json : Json) -> RpcMessage? raise McpError {
  guard json is Object(obj) else {
    raise fail("server sent a JSON value that is not an object")
  }
  let raw_id : Json? = obj.get("id")
  let int_id = match raw_id {
    Some(Number(n, ..)) => Some(n.to_int())
    _ => None
  }
  let rpc_method = match obj.get("method") {
    Some(String(m)) => Some(m)
    _ => None
  }
  let error = match obj.get("error") {
    Some(Object(e)) => {
      let code = match e.get("code") {
        Some(Number(n, ..)) => n.to_int()
        _ => 0
      }
      let message = match e.get("message") {
        Some(String(m)) => m
        _ => "unknown server error"
      }
      Some({ code, message, data: e.get("data"), })
    }
    _ => None
  }
  match (int_id, raw_id, rpc_method) {
    (Some(id), _, None) => Some(Response(id, obj.get("result"), error))
    (_, Some(id), Some(name)) =>
      Some(ServerRequest(id, name, obj.get("params")))
    (_, _, Some(name)) => Some(Notification(name, obj.get("params")))
    (_, _, None) =>
      if error is Some(_) {
        raise fail("server sent an error response without an id")
      } else {
        None
      }
  }
}