///|
/// A handle to the Health API: checks and the healthy instances of a service,
/// including blocking queries that watch a service for membership changes.
pub struct Health {
  client : Client
}

///|
/// A check as reported by the health endpoints: its `check_id`, `name`, current
/// `status`, and `output`.
pub(all) struct HealthCheck {
  node : String
  check_id : String
  name : String?
  status : String?
  notes : String?
  output : String?
  service_id : String?
  service_name : String?
  ns : String?
}

///|
/// One instance of a service as returned by `service` and `connect_service`,
/// paired with the node it runs on and its live checks.
pub(all) struct HealthService {
  id : String
  service : String
  address : String?
  tags : Array[String]
  meta : Map[String, String]?
  ns : String?
  partition : String?
  port : Int32?
  weights : AgentWeights?
}

///|
/// A healthy service instance: the `node` it runs on, the `service` instance,
/// and its `checks`.
pub(all) struct ServiceEntry {
  node : Node
  service : HealthService?
  checks : Array[HealthCheck]
}

///|
/// A `HealthCheck` from its JSON object.
fn health_check_of(j : Json) -> HealthCheck {
  let m = json_map(j)
  {
    node: jstr(m, "Node"),
    check_id: jstr(m, "CheckID"),
    name: jstr_opt(m, "Name"),
    status: jstr_opt(m, "Status"),
    notes: jstr_opt(m, "Notes"),
    output: jstr_opt(m, "Output"),
    service_id: jstr_opt(m, "ServiceID"),
    service_name: jstr_opt(m, "ServiceName"),
    ns: jstr_opt(m, "Namespace"),
  }
}

///|
/// A `HealthService` from its JSON object.
fn health_service_of(j : Json) -> HealthService {
  let m = json_map(j)
  {
    id: jstr(m, "ID"),
    service: jstr(m, "Service"),
    address: jstr_opt(m, "Address"),
    tags: match jstr_array_opt(m, "Tags") {
      Some(t) => t
      None => []
    },
    meta: jmap_opt(m, "Meta"),
    ns: jstr_opt(m, "Namespace"),
    partition: jstr_opt(m, "Partition"),
    port: ji32_opt(m, "Port"),
    weights: weights_of(m, "Weights"),
  }
}

///|
/// An `AgentService` from its JSON object.
fn agent_service_of(j : Json) -> AgentService {
  let m = json_map(j)
  {
    id: jstr(m, "ID"),
    service: jstr(m, "Service"),
    tags: jstr_array_opt(m, "Tags").unwrap_or([]),
    address: jstr_opt(m, "Address"),
    port: ji32_opt(m, "Port"),
    meta: jmap_opt(m, "Meta"),
    ns: jstr_opt(m, "Namespace"),
    weights: weights_of(m, "Weights"),
  }
}

///|
/// A `ServiceEntry` from its JSON object.
fn service_entry_of(j : Json) -> ServiceEntry {
  let m = json_map(j)
  let node = node_of(
    match m.get("Node") {
      Some(n) => n
      None => @json.to_json(jobj())
    },
  )
  let service = match m.get("Service") {
    Some(s) => if s is Object(_) { Some(health_service_of(s)) } else { None }
    None => None
  }
  let checks : Array[HealthCheck] = []
  for c in jarray(m, "Checks") {
    checks.push(health_check_of(c))
  }
  { node, service, checks, }
}

///|
/// An array of `HealthCheck` from a JSON array body.
fn health_checks_of(j : Json) -> Array[HealthCheck] {
  let out : Array[HealthCheck] = []
  for item in json_array(j) {
    out.push(health_check_of(item))
  }
  out
}

///|
/// The checks on `node`.
pub async fn Health::node(
  self : Health,
  node : String,
  options~ : QueryOptions = default_query_options,
) -> (Array[HealthCheck], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/health/node/" + node, options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  (health_checks_of(json), meta)
}

///|
/// The checks belonging to `service`.
pub async fn Health::checks(
  self : Health,
  service : String,
  options~ : QueryOptions = default_query_options,
) -> (Array[HealthCheck], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get(
    "/v1/health/checks/" + service,
    options,
    [],
  ) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  (health_checks_of(json), meta)
}

///|
/// The checks in `state` (`passing`, `warning` or `critical`).
pub async fn Health::checks_in_state(
  self : Health,
  state : String,
  options~ : QueryOptions = default_query_options,
) -> (Array[HealthCheck], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/health/state/" + state, options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  (health_checks_of(json), meta)
}

///|
/// The healthy instances of `service`; `passing` filters to passing instances
/// only. A blocking query (`QueryOptions.wait_index`) watches the set change.
pub async fn Health::service(
  self : Health,
  service : String,
  passing~ : Bool = false,
  options~ : QueryOptions = default_query_options,
) -> (Array[ServiceEntry], QueryMeta) raise Refused {
  let extra : Array[(String, String)] = []
  if passing {
    extra.push(("passing", "true"))
  }
  let (json, meta) = self.client.do_get(
    "/v1/health/service/" + service,
    options,
    extra,
  ) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[ServiceEntry] = []
  for item in json_array(json) {
    out.push(service_entry_of(item))
  }
  (out, meta)
}

///|
/// The checks in `state` (any of the three states).
pub async fn Health::state(
  self : Health,
  state : String,
  options~ : QueryOptions = default_query_options,
) -> (Array[HealthCheck], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/health/state/" + state, options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  (health_checks_of(json), meta)
}

///|
/// Like `service`, but only instances reachable over the connect mesh.
pub async fn Health::connect_service(
  self : Health,
  service : String,
  passing~ : Bool = false,
  options~ : QueryOptions = default_query_options,
) -> (Array[ServiceEntry], QueryMeta) raise Refused {
  let extra : Array[(String, String)] = []
  if passing {
    extra.push(("passing", "true"))
  }
  let (json, meta) = self.client.do_get(
    "/v1/health/connect/" + service,
    options,
    extra,
  ) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[ServiceEntry] = []
  for item in json_array(json) {
    out.push(service_entry_of(item))
  }
  (out, meta)
}