///|
/// The server information advertised in the `INFO` handshake line and on
/// every cluster topology change. Fields the client acts on are decoded;
/// the rest is available through `raw`.
pub(all) struct ServerInfo {
  server_id : String
  server_name : String
  version : String
  host : String
  port : Int
  /// True when the server requires authentication.
  auth_required : Bool
  /// True when the server supports HMSG headers.
  headers : Bool
  /// Maximum protocol payload size in bytes; 0 means unlimited.
  max_payload : Int
  /// Minimum server version supporting headers, as advertised.
  proto : Int
} derive(Eq, Debug)

///|
/// Decode the JSON argument of an `INFO` operation. Returns an error when
/// the JSON is malformed or missing the fields the client cannot proceed
/// without (`server_id`, `version`, `host`, `port`).
pub fn parse_server_info(json : String) -> Result[ServerInfo, String] {
  let value = @json.parse(json) catch {
    e => return Err("invalid INFO json: \u{22}\{e}\u{22}")
  }
  guard value is Object(fields) else {
    return Err("INFO json is not an object")
  }
  let server_id = str_field(fields, "server_id")
  let version = str_field(fields, "version")
  let host = str_field(fields, "host")
  let port = int_field(fields, "port")
  if server_id.is_empty() || version.is_empty() {
    return Err("INFO json missing server_id or version")
  }
  let port = match port {
    Ok(p) => p
    Err(msg) => return Err(msg)
  }
  Ok({
    server_id,
    server_name: str_field(fields, "server_name"),
    version,
    host,
    port,
    auth_required: bool_field(fields, "auth_required"),
    headers: bool_field(fields, "headers"),
    max_payload: match int_field(fields, "max_payload") {
      Ok(n) => n
      Err(_) => 0
    },
    proto: match int_field(fields, "proto") {
      Ok(n) => n
      Err(_) => 1
    },
  })
}

///|
/// Field readers tolerate absent keys with typed defaults; only fields with
/// the wrong JSON type are errors.
///
fn str_field(fields : Map[String, Json], key : String) -> String {
  match fields.get(key) {
    Some(Json::String(s)) => s
    _ => ""
  }
}

///|
fn bool_field(fields : Map[String, Json], key : String) -> Bool {
  match fields.get(key) {
    Some(Json::True) => true
    Some(Json::False) => false
    _ => false
  }
}

///|
fn int_field(fields : Map[String, Json], key : String) -> Result[Int, String] {
  match fields.get(key) {
    None => Ok(0)
    Some(Json::Number(n, ..)) => Ok(n.to_int())
    Some(_) => Err("INFO field \u{22}" + key + "\u{22} is not a number")
  }
}

///|
/// Decode the `INFO` carried by a parsed `ServerOp::Info`.
pub fn server_info_from_op(op : ServerOp) -> Result[ServerInfo, String] {
  guard op is Info(json) else { return Err("not an INFO operation") }
  parse_server_info(json)
}