///|
/// A handle to the Catalog API: datacenter-wide registration of nodes and
/// services, and read-only listing of nodes, services and their metadata.
pub struct Catalog {
  client : Client
}

///|
/// A Consul node: its `node` name, `address`, and free-form `meta`.
pub(all) struct Node {
  node : String
  address : String
  meta : Map[String, String]?
  ns : String?
  partition : String?
  tagged_addresses : Map[String, String]?
  create_index : UInt64?
  modify_index : UInt64?
  deregister_critical_service_after : String?
}

///|
/// The body of `PUT /v1/catalog/register`: a `node` and `address`, optional
/// `node_meta`, and at most one service and check (or several checks) to register
/// against that node.
pub(all) struct CatalogRegistration {
  node : String
  address : String
  node_meta : Map[String, String]?
  ns : String?
  partition : String?
  service : CatalogService?
  check : AgentCheck?
  checks : Array[AgentCheck]?
}

///|
/// The body of `PUT /v1/catalog/deregister`: identifies a node, service or check
/// to remove.
pub(all) struct CatalogDeregistration {
  node : String
  address : String?
  service_id : String?
  check_id : String?
  ns : String?
  partition : String?
}

///|
/// A service as listed by the catalog: its `id`, the `node` it runs on, and its
/// tags, `meta` and `port`.
pub(all) struct CatalogService {
  id : String
  node : String
  address : String
  service : String
  tags : Array[String]
  meta : Map[String, String]?
  ns : String?
  partition : String?
  port : Int32?
  weights : AgentWeights?
  create_index : UInt64?
  modify_index : UInt64?
}

///|
/// A single service bound to a node, as returned by `node`.
pub(all) struct CatalogNodeService {
  id : String
  service : String
  tags : Array[String]
  meta : Map[String, String]?
  ns : String?
  port : Int32?
  address : String?
  weights : AgentWeights?
}

///|
/// A node together with every service registered on it.
pub(all) struct CatalogNode {
  node : Node
  services : Map[String, CatalogNodeService]?
}

///|
/// A `Node` from its JSON object.
pub fn node_of(j : Json) -> Node {
  let m = json_map(j)
  {
    node: jstr(m, "Node"),
    address: jstr(m, "Address"),
    meta: jmap_opt(m, "Meta"),
    ns: jstr_opt(m, "Namespace"),
    partition: jstr_opt(m, "Partition"),
    tagged_addresses: jmap_opt(m, "TaggedAddresses"),
    create_index: ju64_opt(m, "CreateIndex"),
    modify_index: ju64_opt(m, "ModifyIndex"),
    deregister_critical_service_after: jstr_opt(
      m, "DeregisterCriticalServiceAfter",
    ),
  }
}

///|
/// `AgentWeights` from the `key` object in `m`, or `None`.
fn weights_of(m : Map[String, Json], key : String) -> AgentWeights? {
  match m.get(key) {
    Some(Object(o)) =>
      Some({ passing: ji32_opt(o, "Passing"), warning: ji32_opt(o, "Warning"), })
    _ => None
  }
}

///|
/// An `AgentCheck` as the JSON object a registration carries.
fn agent_check_to_json(c : AgentCheck) -> Json {
  let m = jobj()
  put_str(m, "CheckID", Some(c.id))
  put_str(m, "Name", c.name)
  put_str(m, "Status", c.status)
  put_str(m, "Notes", c.notes)
  put_str(m, "Output", c.output)
  put_str(m, "ServiceID", c.service_id)
  put_str(m, "ServiceName", c.service_name)
  put_str(m, "Namespace", c.ns)
  @json.to_json(m)
}

///|
/// A `CatalogService` as the JSON object a register body carries.
fn catalog_service_to_json(s : CatalogService) -> Json {
  let m = jobj()
  put_str(m, "ID", Some(s.id))
  put_str(m, "Service", Some(s.service))
  put_str_array(m, "Tags", Some(s.tags))
  put_str(m, "Address", Some(s.address))
  put_i32(m, "Port", s.port)
  match s.meta {
    Some(mm) => m["Meta"] = map_to_json(mm)
    None => ()
  }
  @json.to_json(m)
}

///|
/// A `CatalogRegistration` as its write body.
fn catalog_registration_to_json(r : CatalogRegistration) -> Json {
  let m = jobj()
  put_str(m, "Node", Some(r.node))
  put_str(m, "Address", Some(r.address))
  match r.node_meta {
    Some(mm) => m["NodeMeta"] = map_to_json(mm)
    None => ()
  }
  put_str(m, "Namespace", r.ns)
  put_str(m, "Partition", r.partition)
  match r.service {
    Some(s) => m["Service"] = catalog_service_to_json(s)
    None => ()
  }
  match r.check {
    Some(c) => m["Check"] = agent_check_to_json(c)
    None => ()
  }
  match r.checks {
    Some(cs) => {
      let arr : Array[Json] = []
      for c in cs {
        arr.push(agent_check_to_json(c))
      }
      m["Checks"] = @json.to_json(arr)
    }
    None => ()
  }
  @json.to_json(m)
}

///|
/// A `CatalogDeregistration` as its write body.
fn catalog_dereg_to_json(d : CatalogDeregistration) -> Json {
  let m = jobj()
  put_str(m, "Node", Some(d.node))
  put_str(m, "Address", d.address)
  put_str(m, "ServiceID", d.service_id)
  put_str(m, "CheckID", d.check_id)
  put_str(m, "Namespace", d.ns)
  put_str(m, "Partition", d.partition)
  @json.to_json(m)
}

///|
/// A `CatalogService` from its JSON object.
fn catalog_service_of(j : Json) -> CatalogService {
  let m = json_map(j)
  let id = if jstr(m, "ServiceID") != "" {
    jstr(m, "ServiceID")
  } else {
    jstr(m, "ID")
  }
  {
    id,
    node: jstr(m, "Node"),
    address: jstr(m, "ServiceAddress"),
    service: jstr(m, "ServiceName"),
    tags: match jstr_array_opt(m, "ServiceTags") {
      Some(t) => t
      None => []
    },
    meta: jmap_opt(m, "ServiceMeta"),
    ns: jstr_opt(m, "Namespace"),
    partition: jstr_opt(m, "Partition"),
    port: ji32_opt(m, "ServicePort"),
    weights: weights_of(m, "ServiceWeights"),
    create_index: ju64_opt(m, "CreateIndex"),
    modify_index: ju64_opt(m, "ModifyIndex"),
  }
}

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

///|
/// A `CatalogNode` from its JSON object.
fn catalog_node_of(j : Json) -> CatalogNode {
  let m = json_map(j)
  let node = node_of(
    match m.get("Node") {
      Some(n) => n
      None => @json.to_json(jobj())
    },
  )
  let services = match m.get("Services") {
    Some(Object(o)) => {
      let out : Map[String, CatalogNodeService] = Map([])
      for k, v in o {
        out[k] = catalog_node_service_of(v)
      }
      Some(out)
    }
    _ => None
  }
  { node, services, }
}

///|
/// Register `reg` against the catalog.
pub async fn Catalog::register(
  self : Catalog,
  reg : CatalogRegistration,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let body = @utf8.encode(catalog_registration_to_json(reg).stringify())
  let (_, meta) = self.client.do_put("/v1/catalog/register", body, options, [])
  meta
}

///|
/// Deregister `dereg` from the catalog.
pub async fn Catalog::deregister(
  self : Catalog,
  dereg : CatalogDeregistration,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let body = @utf8.encode(catalog_dereg_to_json(dereg).stringify())
  let (_, meta) = self.client.do_put("/v1/catalog/deregister", body, options, [])
  meta
}

///|
/// The datacenters in the WAN federation.
pub async fn Catalog::datacenters(
  self : Catalog,
  options~ : QueryOptions = default_query_options,
) -> (Array[String], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/catalog/datacenters", options, [])
  let out : Array[String] = []
  for item in json_array(json) {
    if item is String(s) {
      out.push(s)
    }
  }
  (out, meta)
}

///|
/// The nodes in this datacenter, narrowed by `options.filter`.
pub async fn Catalog::nodes(
  self : Catalog,
  options~ : QueryOptions = default_query_options,
) -> (Array[Node], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/catalog/nodes", options, [])
  let out : Array[Node] = []
  for item in json_array(json) {
    out.push(node_of(item))
  }
  (out, meta)
}

///|
/// One node and every service registered on it.
pub async fn Catalog::node(
  self : Catalog,
  node : String,
  options~ : QueryOptions = default_query_options,
) -> (CatalogNode, QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/catalog/node/" + node, options, []) catch {
    Refused::NotFound =>
      return (
        { node: node_of(@json.to_json(jobj())), services: None, },
        empty_query_meta(),
      )
    e => raise e
  }
  (catalog_node_of(json), meta)
}

///|
/// The service names in this datacenter, each with its tag set.
pub async fn Catalog::services(
  self : Catalog,
  options~ : QueryOptions = default_query_options,
) -> (Map[String, Array[String]], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/catalog/services", options, [])
  let out : Map[String, Array[String]] = Map([])
  for k, v in json_map(json) {
    let tags : Array[String] = []
    for item in json_array(v) {
      if item is String(s) {
        tags.push(s)
      }
    }
    out[k] = tags
  }
  (out, meta)
}

///|
/// The instances of `service` in this datacenter.
pub async fn Catalog::service(
  self : Catalog,
  service : String,
  options~ : QueryOptions = default_query_options,
) -> (Array[CatalogService], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get(
    "/v1/catalog/service/" + service,
    options,
    [],
  ) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[CatalogService] = []
  for item in json_array(json) {
    out.push(catalog_service_of(item))
  }
  (out, meta)
}