///|
/// A handle to the Agent API: register and deregister services and checks on
/// this agent, keep TTL checks alive, read the agent's own state, and manage
/// cluster membership and maintenance mode.
pub struct Agent {
  client : Client
}

///|
/// A health check registered alongside a service or on its own. One of `ttl`,
/// `interval`+`script`/`http`/`tcp`/`grpc`, or `alias_*` selects the check
/// kind; `deregister_critical_service_after` lets a failing check auto-remove
/// its service.
pub(all) struct AgentServiceCheck {
  id : String?
  name : String?
  notes : String?
  service_id : String?
  status : String?
  ttl : String?
  interval : String?
  timeout : String?
  http : String?
  tls_skip_verify : Bool?
  http_method : String?
  tcp : String?
  grpc : String?
  grpc_use_tls : Bool?
  header : Map[String, Array[String]]?
  deregister_critical_service_after : String?
  alias_node : String?
  alias_service : String?
  success_before_passing : Int32?
  failures_before_critical : Int32?
}

///|
/// The primary and passing weights of a service, used by DNS and prepared
/// queries. `passing` defaults to 1 and `warning` to 1 when unset.
pub(all) struct AgentWeights {
  passing : Int32?
  warning : Int32?
}

///|
/// A service registered on this agent. `id` defaults to `name` when empty; `tags`
/// and `meta` are free-form; `address` and `port` are where the instance listens
/// (when unset, the agent's address is used).
pub(all) struct AgentService {
  id : String
  service : String
  tags : Array[String]
  address : String?
  port : Int32?
  meta : Map[String, String]?
  ns : String?
  weights : AgentWeights?
}

///|
/// The body of `PUT /v1/agent/service/register`: a service plus at most one
/// inline check (`check`) or several (`checks`); `enable_tag_override` lets the
/// agent's tags be overridden by the catalog.
pub(all) struct AgentServiceRegistration {
  id : String?
  name : String
  tags : Array[String]?
  address : String?
  port : Int32?
  meta : Map[String, String]?
  ns : String?
  check : AgentServiceCheck?
  checks : Array[AgentServiceCheck]?
  enable_tag_override : Bool?
  weights : AgentWeights?
}

///|
/// A check with nothing set, to update the fields a check needs.
pub let default_service_check : AgentServiceCheck = {
  id: None,
  name: None,
  notes: None,
  service_id: None,
  status: None,
  ttl: None,
  interval: None,
  timeout: None,
  http: None,
  tls_skip_verify: None,
  http_method: None,
  tcp: None,
  grpc: None,
  grpc_use_tls: None,
  header: None,
  deregister_critical_service_after: None,
  alias_node: None,
  alias_service: None,
  success_before_passing: None,
  failures_before_critical: None,
}

///|
/// A registration with nothing set but an empty name, to update the fields a service needs.
pub let default_service_registration : AgentServiceRegistration = {
  id: None,
  name: "",
  tags: None,
  address: None,
  port: None,
  meta: None,
  ns: None,
  check: None,
  checks: None,
  enable_tag_override: None,
  weights: None,
}

///|
/// The body of `PUT /v1/agent/check/register`: the identity fields plus the
/// check definition folded in as `definition`.
pub(all) struct AgentCheckRegistration {
  id : String?
  name : String
  notes : String?
  service_id : String?
  service_name : String?
  ns : String?
  status : String?
  definition : AgentServiceCheck
}

///|
/// A check as reported by `self` and the check endpoints: its id, name, current
/// `status` (`passing`, `warning`, `critical`), `output`, and the service it
/// guards.
pub(all) struct AgentCheck {
  id : String
  name : String?
  status : String?
  notes : String?
  output : String?
  service_id : String?
  service_name : String?
  ns : String?
}

///|
/// The body of `PUT /v1/agent/check/update`: set a TTL check's `status` and
/// `output` without sending a fresh TTL. `check_id` names the check to update.
pub(all) struct AgentCheckUpdate {
  check_id : String
  status : String
  output : String?
}

///|
/// A cluster member as seen by the agent's gossip layer: its `name`, `addr`,
/// `port`, `status`, and the `tags` it gossips (including `role`, `dc`, `vsn`).
pub(all) struct AgentMember {
  name : String
  addr : String
  port : Int32
  tags : Map[String, String]?
  status : String?
}

///|
/// The agent's own state: its `config`, its `agent_member` record, and `stats`.
pub(all) struct AgentSelf {
  config : Json
  agent_member : AgentMember
  stats : Json
}

///|
/// An `AgentWeights` as a JSON object.
fn agent_weights_to_json(w : AgentWeights) -> Json {
  let m = jobj()
  put_i32(m, "Passing", w.passing)
  put_i32(m, "Warning", w.warning)
  @json.to_json(m)
}

///|
/// A `Map[String, Array[String]]` (check headers) as a JSON object.
fn headers_to_json(h : Map[String, Array[String]]) -> Json {
  let out : Map[String, Json] = Map([])
  for k, v in h {
    let arr : Array[Json] = []
    for s in v {
      arr.push(@json.to_json(s))
    }
    out[k] = @json.to_json(arr)
  }
  @json.to_json(out)
}

///|
/// An `AgentServiceCheck` as a JSON object.
fn agent_service_check_to_json(c : AgentServiceCheck) -> Json {
  let m = jobj()
  put_str(m, "CheckID", c.id)
  put_str(m, "Name", c.name)
  put_str(m, "Notes", c.notes)
  put_str(m, "ServiceID", c.service_id)
  put_str(m, "Status", c.status)
  put_str(m, "TTL", c.ttl)
  put_str(m, "Interval", c.interval)
  put_str(m, "Timeout", c.timeout)
  put_str(m, "HTTP", c.http)
  put_bool(m, "TLSSkipVerify", c.tls_skip_verify)
  put_str(m, "Method", c.http_method)
  put_str(m, "TCP", c.tcp)
  put_str(m, "GRPC", c.grpc)
  put_bool(m, "GRPCUseTLS", c.grpc_use_tls)
  match c.header {
    Some(h) => m["Header"] = headers_to_json(h)
    None => ()
  }
  put_str(
    m,
    "DeregisterCriticalServiceAfter",
    c.deregister_critical_service_after,
  )
  put_str(m, "AliasNode", c.alias_node)
  put_str(m, "AliasService", c.alias_service)
  put_i32(m, "SuccessBeforePassing", c.success_before_passing)
  put_i32(m, "FailuresBeforeCritical", c.failures_before_critical)
  @json.to_json(m)
}

///|
/// An `AgentServiceRegistration` as its write body.
fn agent_service_registration_to_json(r : AgentServiceRegistration) -> Json {
  let m = jobj()
  put_str(m, "ID", r.id)
  put_str(m, "Name", Some(r.name))
  put_str_array(m, "Tags", r.tags)
  put_str(m, "Address", r.address)
  put_i32(m, "Port", r.port)
  match r.meta {
    Some(mm) => m["Meta"] = map_to_json(mm)
    None => ()
  }
  put_str(m, "Namespace", r.ns)
  match r.check {
    Some(c) => m["Check"] = agent_service_check_to_json(c)
    None => ()
  }
  match r.checks {
    Some(cs) => {
      let arr : Array[Json] = []
      for c in cs {
        arr.push(agent_service_check_to_json(c))
      }
      m["Checks"] = @json.to_json(arr)
    }
    None => ()
  }
  put_bool(m, "EnableTagOverride", r.enable_tag_override)
  match r.weights {
    Some(w) => m["Weights"] = agent_weights_to_json(w)
    None => ()
  }
  @json.to_json(m)
}

///|
/// An `AgentCheckRegistration` as its write body (the definition folded in).
fn agent_check_registration_to_json(r : AgentCheckRegistration) -> Json {
  let m = json_map(agent_service_check_to_json(r.definition))
  put_str(m, "CheckID", r.id)
  put_str(m, "Name", Some(r.name))
  put_str(m, "Notes", r.notes)
  put_str(m, "ServiceID", r.service_id)
  put_str(m, "ServiceName", r.service_name)
  put_str(m, "Namespace", r.ns)
  put_str(m, "Status", r.status)
  @json.to_json(m)
}

///|
/// An `AgentMember` from its JSON object.
fn agent_member_of(j : Json) -> AgentMember {
  let m = json_map(j)
  {
    name: jstr(m, "Name"),
    addr: jstr(m, "Addr"),
    port: ji32(m, "Port"),
    tags: jmap_opt(m, "Tags"),
    status: jstr_opt(m, "Status"),
  }
}

///|
/// An `AgentSelf` from its JSON object.
fn agent_self_of(j : Json) -> AgentSelf {
  let m = json_map(j)
  let mem_rec = agent_member_of(
    match m.get("Member") {
      Some(x) => x
      None => @json.to_json(jobj())
    },
  )
  {
    config: match m.get("Config") {
      Some(c) => c
      None => @json.to_json(jobj())
    },
    agent_member: mem_rec,
    stats: match m.get("Stats") {
      Some(s) => s
      None => @json.to_json(jobj())
    },
  }
}

///|
/// Register `reg` with this agent. A service with an inline TTL check starts
/// `critical`; follow with `check_pass` to make it healthy.
pub async fn Agent::service_register(
  self : Agent,
  reg : AgentServiceRegistration,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let body = @utf8.encode(agent_service_registration_to_json(reg).stringify())
  let (_, meta) = self.client.do_put(
    "/v1/agent/service/register",
    body,
    options,
    [],
  )
  meta
}

///|
/// Deregister the service instance `id` from this agent.
pub async fn Agent::service_deregister(
  self : Agent,
  id : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_put(
    "/v1/agent/service/deregister/" + id,
    b"",
    options,
    [],
  )
  meta
}

///|
/// Register `reg` (a check, optionally bound to a service) with this agent.
pub async fn Agent::check_register(
  self : Agent,
  reg : AgentCheckRegistration,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let body = @utf8.encode(agent_check_registration_to_json(reg).stringify())
  let (_, meta) = self.client.do_put(
    "/v1/agent/check/register",
    body,
    options,
    [],
  )
  meta
}

///|
/// Deregister the check `check_id` from this agent.
pub async fn Agent::check_deregister(
  self : Agent,
  check_id : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_put(
    "/v1/agent/check/deregister/" + check_id,
    b"",
    options,
    [],
  )
  meta
}

///|
/// Mark the TTL/http/tcp/grpc check `check_id` passing; the keep-alive that
/// renews a service's lease.
pub async fn Agent::check_pass(
  self : Agent,
  check_id : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_put(
    "/v1/agent/check/pass/" + check_id,
    b"",
    options,
    [],
  )
  meta
}

///|
/// Mark the check `check_id` failing.
pub async fn Agent::check_fail(
  self : Agent,
  check_id : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_put(
    "/v1/agent/check/fail/" + check_id,
    b"",
    options,
    [],
  )
  meta
}

///|
/// Set a TTL check's `status` and `output` without resetting its TTL clock.
pub async fn Agent::check_update(
  self : Agent,
  update : AgentCheckUpdate,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let m = jobj()
  put_str(m, "Status", Some(update.status))
  put_str(m, "Output", update.output)
  let body = @utf8.encode(@json.to_json(m).stringify())
  let (_, meta) = self.client.do_put(
    "/v1/agent/check/update/" + update.check_id,
    body,
    options,
    [],
  )
  meta
}

///|
/// The agent's own configuration, member record and runtime stats.
pub async fn Agent::self(
  self : Agent,
  options~ : QueryOptions = default_query_options,
) -> (AgentSelf, QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/agent/self", options, [])
  (agent_self_of(json), meta)
}

///|
/// The services registered with this agent, by id.
pub async fn Agent::services(
  self : Agent,
  options~ : QueryOptions = default_query_options,
) -> (Map[String, AgentService], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/agent/services", options, [])
  let out : Map[String, AgentService] = Map([])
  guard json is Object(entries) else {
    raise Refused::Json("agent services: expected an object")
  }
  for id, value in entries {
    out[id] = agent_service_of(value)
  }
  (out, meta)
}

///|
/// The checks registered with this agent, by id.
pub async fn Agent::checks(
  self : Agent,
  options~ : QueryOptions = default_query_options,
) -> (Map[String, HealthCheck], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/agent/checks", options, [])
  let out : Map[String, HealthCheck] = Map([])
  guard json is Object(entries) else {
    raise Refused::Json("agent checks: expected an object")
  }
  for id, value in entries {
    out[id] = health_check_of(value)
  }
  (out, meta)
}

///|
/// The cluster members this agent gossips with. `options.filter` (a
/// go-compatible tag expression) narrows the returned set.
pub async fn Agent::members(
  self : Agent,
  options~ : QueryOptions = default_query_options,
) -> (Array[AgentMember], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/agent/members", options, [])
  let out : Array[AgentMember] = []
  for item in json_array(json) {
    out.push(agent_member_of(item))
  }
  (out, meta)
}

///|
/// Like `members`, but `filter` is required rather than optional.
pub async fn Agent::members_with_filter(
  self : Agent,
  filter : String,
  options~ : QueryOptions = default_query_options,
) -> (Array[AgentMember], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/agent/members", options, [
    ("filter", filter),
  ])
  let out : Array[AgentMember] = []
  for item in json_array(json) {
    out.push(agent_member_of(item))
  }
  (out, meta)
}

///|
/// Ask the agent at `address` to join the cluster; `wan` joins over the WAN
/// gossip pool rather than the LAN pool.
pub async fn Agent::join(
  self : Agent,
  address : String,
  wan~ : Bool = false,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let extra : Array[(String, String)] = []
  if wan {
    extra.push(("wan", "1"))
  }
  let (_, meta) = self.client.do_put(
    "/v1/agent/join/" + address,
    b"",
    options,
    extra,
  )
  meta
}

///|
/// Ask this agent to leave the cluster gracefully.
pub async fn Agent::leave(
  self : Agent,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_put("/v1/agent/leave", b"", options, [])
  meta
}

///|
/// Force a `node` out of the cluster without its cooperation.
pub async fn Agent::force_leave(
  self : Agent,
  node : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_put(
    "/v1/agent/force-leave/" + node,
    b"",
    options,
    [],
  )
  meta
}

///|
/// Put the service instance `service_id` into maintenance mode, marking its
/// checks `maintenance` with `reason`; the instance stays registered but looks
/// down to health checks.
pub async fn Agent::enable_service_maintenance(
  self : Agent,
  service_id : String,
  reason~ : String = "",
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let extra : Array[(String, String)] = [("enable", "true")]
  if reason != "" {
    extra.push(("reason", reason))
  }
  let (_, meta) = self.client.do_put(
    "/v1/agent/service/maintenance/" + service_id,
    b"",
    options,
    extra,
  )
  meta
}

///|
/// Take the service instance `service_id` out of maintenance mode.
pub async fn Agent::disable_service_maintenance(
  self : Agent,
  service_id : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_put(
    "/v1/agent/service/maintenance/" + service_id,
    b"",
    options,
    [("enable", "false")],
  )
  meta
}

///|
/// Put the whole node into maintenance mode.
pub async fn Agent::enable_node_maintenance(
  self : Agent,
  reason~ : String = "",
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let extra : Array[(String, String)] = [("enable", "true")]
  if reason != "" {
    extra.push(("reason", reason))
  }
  let (_, meta) = self.client.do_put(
    "/v1/agent/node/maintenance", b"", options, extra,
  )
  meta
}

///|
/// Take the node out of maintenance mode.
pub async fn Agent::disable_node_maintenance(
  self : Agent,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_put(
    "/v1/agent/node/maintenance",
    b"",
    options,
    [("enable", "false")],
  )
  meta
}

///|
/// Replace the agent's ACL token (used when the local token must change without
/// restarting the agent).
pub async fn Agent::update_acl_token(
  self : Agent,
  token : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let m = jobj()
  put_str(m, "Token", Some(token))
  let body = @utf8.encode(@json.to_json(m).stringify())
  let (_, meta) = self.client.do_put(
    "/v1/agent/token/acl_token",
    body,
    options,
    [],
  )
  meta
}