///|
/// A handle to the Config Entries API: typed, centrally-stored configuration
/// objects (service defaults, routers, splitters, resolvers, mesh-wide
/// settings) that Consul serves to the data plane.
pub struct ConfigEntries {
  client : Client
}

///|
/// A config entry. Each variant carries the typed entry for one `kind`; a read
/// returns the variant for the stored `kind`, and a write carries the variant
/// to store. `kind` and `name` are read from the wrapped entry.
pub(all) enum ConfigEntry {
  ServiceDefault(ServiceDefaultConfigEntry)
  ServiceRouter(ServiceRouterConfigEntry)
  ServiceSplitter(ServiceSplitterConfigEntry)
  ServiceResolver(ServiceResolverConfigEntry)
  Mesh(MeshConfigEntry)
}

///|
/// Default protocol, mesh and per-service settings.
pub(all) struct ServiceDefaultConfigEntry {
  name : String
  ns : String?
  partition : String?
  protocol : String?
  mesh_gateway : ServiceMeshGateway?
}

///|
/// A service's mesh-gateway mode.
pub(all) struct ServiceMeshGateway {
  mode : String?
}

///|
/// Layer-7 routing rules for a service.
pub(all) struct ServiceRouterConfigEntry {
  name : String
  ns : String?
  partition : String?
  routes : Array[ServiceRoute]?
}

///|
/// One routing match/rewrite rule.
pub(all) struct ServiceRoute {
  matcher : ServiceRouteMatch?
  destination : ServiceRouteDestination?
}

///|
/// The match half of a route.
pub(all) struct ServiceRouteMatch {
  http : ServiceRouteHTTPMatch?
}

///|
/// HTTP match fields.
pub(all) struct ServiceRouteHTTPMatch {
  path : String?
  prefix : String?
  regex : String?
}

///|
/// The destination half of a route.
pub(all) struct ServiceRouteDestination {
  service : String?
  ns : String?
}

///|
/// Weighted traffic split for a service.
pub(all) struct ServiceSplitterConfigEntry {
  name : String
  ns : String?
  partition : String?
  splits : Array[ServiceSplit]?
}

///|
/// One weighted destination in a split.
pub(all) struct ServiceSplit {
  weight : Double
  service : String?
  ns : String?
}

///|
/// How a service resolves to healthy instances, with failover.
pub(all) struct ServiceResolverConfigEntry {
  name : String
  ns : String?
  partition : String?
  datacenter : String?
  service : String?
  failover : QueryDatacenterOptions?
  redirect : String?
}

///|
/// Mesh-wide configuration.
pub(all) struct MeshConfigEntry {
  name : String
  partition : String?
  transparent_proxy : MeshTransparentProxy?
}

///|
/// Transparent-proxy settings for the mesh.
pub(all) struct MeshTransparentProxy {
  mode : String?
}

///|
/// The `Kind` string for a config entry.
fn config_entry_kind(entry : ConfigEntry) -> String {
  match entry {
    ServiceDefault(_) => "service-defaults"
    ServiceRouter(_) => "service-router"
    ServiceSplitter(_) => "service-splitter"
    ServiceResolver(_) => "service-resolver"
    Mesh(_) => "mesh"
  }
}

///|
/// A `ServiceMeshGateway` as a JSON object.
fn mesh_gateway_to_json(g : ServiceMeshGateway) -> Json {
  let m = jobj()
  put_str(m, "Mode", g.mode)
  @json.to_json(m)
}

///|
/// A `ServiceMeshGateway` from a JSON object.
fn mesh_gateway_of(j : Json) -> ServiceMeshGateway {
  { mode: jstr_opt(json_map(j), "Mode"), }
}

///|
/// A `ServiceRouteHTTPMatch` as a JSON object.
fn route_http_match_to_json(h : ServiceRouteHTTPMatch) -> Json {
  let m = jobj()
  put_str(m, "PathExact", h.path)
  put_str(m, "PathPrefix", h.prefix)
  put_str(m, "PathRegex", h.regex)
  @json.to_json(m)
}

///|
/// A `ServiceRouteHTTPMatch` from a JSON object.
fn route_http_match_of(j : Json) -> ServiceRouteHTTPMatch {
  let m = json_map(j)
  {
    path: jstr_opt(m, "PathExact"),
    prefix: jstr_opt(m, "PathPrefix"),
    regex: jstr_opt(m, "PathRegex"),
  }
}

///|
/// A `ServiceRouteDestination` as a JSON object.
fn route_destination_to_json(d : ServiceRouteDestination) -> Json {
  let m = jobj()
  put_str(m, "Service", d.service)
  put_str(m, "Namespace", d.ns)
  @json.to_json(m)
}

///|
/// A `ServiceRouteDestination` from a JSON object.
fn route_destination_of(j : Json) -> ServiceRouteDestination {
  let m = json_map(j)
  { service: jstr_opt(m, "Service"), ns: jstr_opt(m, "Namespace"), }
}

///|
/// A `ServiceRoute` as a JSON object.
fn route_to_json(r : ServiceRoute) -> Json {
  let m = jobj()
  match r.matcher {
    Some(mt) => {
      let mm = jobj()
      match mt.http {
        Some(h) => mm["HTTP"] = route_http_match_to_json(h)
        None => ()
      }
      m["Match"] = @json.to_json(mm)
    }
    None => ()
  }
  match r.destination {
    Some(d) => m["Destination"] = route_destination_to_json(d)
    None => ()
  }
  @json.to_json(m)
}

///|
/// A `ServiceRoute` from a JSON object.
fn route_of(j : Json) -> ServiceRoute {
  let m = json_map(j)
  let matcher = match m.get("Match") {
    Some(mt) =>
      if mt is Object(mm) {
        Some({
          http: match mm.get("HTTP") {
            Some(h) =>
              if h is Object(_) {
                Some(route_http_match_of(h))
              } else {
                None
              }
            None => None
          },
        })
      } else {
        None
      }
    None => None
  }
  let destination = match m.get("Destination") {
    Some(d) => if d is Object(_) { Some(route_destination_of(d)) } else { None }
    None => None
  }
  { matcher, destination, }
}

///|
/// A `ServiceDefaultConfigEntry` as a JSON object.
fn default_entry_to_json(e : ServiceDefaultConfigEntry) -> Json {
  let m = jobj()
  m["Kind"] = @json.to_json("service-defaults")
  put_str(m, "Name", Some(e.name))
  put_str(m, "Namespace", e.ns)
  put_str(m, "Partition", e.partition)
  put_str(m, "Protocol", e.protocol)
  match e.mesh_gateway {
    Some(g) => m["MeshGateway"] = mesh_gateway_to_json(g)
    None => ()
  }
  @json.to_json(m)
}

///|
/// A `ServiceDefaultConfigEntry` from a JSON object.
fn default_entry_of(m : Map[String, Json]) -> ServiceDefaultConfigEntry {
  {
    name: jstr(m, "Name"),
    ns: jstr_opt(m, "Namespace"),
    partition: jstr_opt(m, "Partition"),
    protocol: jstr_opt(m, "Protocol"),
    mesh_gateway: match m.get("MeshGateway") {
      Some(g) => if g is Object(_) { Some(mesh_gateway_of(g)) } else { None }
      None => None
    },
  }
}

///|
/// A `ServiceRouterConfigEntry` as a JSON object.
fn router_entry_to_json(e : ServiceRouterConfigEntry) -> Json {
  let m = jobj()
  m["Kind"] = @json.to_json("service-router")
  put_str(m, "Name", Some(e.name))
  put_str(m, "Namespace", e.ns)
  put_str(m, "Partition", e.partition)
  match e.routes {
    Some(rs) => {
      let arr : Array[Json] = []
      for r in rs {
        arr.push(route_to_json(r))
      }
      m["Routes"] = @json.to_json(arr)
    }
    None => ()
  }
  @json.to_json(m)
}

///|
/// A `ServiceRouterConfigEntry` from a JSON object.
fn router_entry_of(m : Map[String, Json]) -> ServiceRouterConfigEntry {
  let routes : Array[ServiceRoute] = []
  for item in jarray(m, "Routes") {
    routes.push(route_of(item))
  }
  {
    name: jstr(m, "Name"),
    ns: jstr_opt(m, "Namespace"),
    partition: jstr_opt(m, "Partition"),
    routes: if routes.length() == 0 {
      None
    } else {
      Some(routes)
    },
  }
}

///|
/// A `ServiceSplitterConfigEntry` as a JSON object.
fn splitter_entry_to_json(e : ServiceSplitterConfigEntry) -> Json {
  let m = jobj()
  m["Kind"] = @json.to_json("service-splitter")
  put_str(m, "Name", Some(e.name))
  put_str(m, "Namespace", e.ns)
  put_str(m, "Partition", e.partition)
  match e.splits {
    Some(sp) => {
      let arr : Array[Json] = []
      for s in sp {
        let sm = jobj()
        sm["Weight"] = @json.to_json(s.weight)
        put_str(sm, "Service", s.service)
        put_str(sm, "Namespace", s.ns)
        arr.push(@json.to_json(sm))
      }
      m["Splits"] = @json.to_json(arr)
    }
    None => ()
  }
  @json.to_json(m)
}

///|
/// A `ServiceSplitterConfigEntry` from a JSON object.
fn splitter_entry_of(m : Map[String, Json]) -> ServiceSplitterConfigEntry {
  let splits : Array[ServiceSplit] = []
  for item in jarray(m, "Splits") {
    let sm = json_map(item)
    splits.push({
      weight: match jdouble_opt(sm, "Weight") {
        Some(d) => d
        None => 0.0
      },
      service: jstr_opt(sm, "Service"),
      ns: jstr_opt(sm, "Namespace"),
    })
  }
  {
    name: jstr(m, "Name"),
    ns: jstr_opt(m, "Namespace"),
    partition: jstr_opt(m, "Partition"),
    splits: if splits.length() == 0 {
      None
    } else {
      Some(splits)
    },
  }
}

///|
/// A `ServiceResolverConfigEntry` as a JSON object.
fn resolver_entry_to_json(e : ServiceResolverConfigEntry) -> Json {
  let m = jobj()
  m["Kind"] = @json.to_json("service-resolver")
  put_str(m, "Name", Some(e.name))
  put_str(m, "Namespace", e.ns)
  put_str(m, "Partition", e.partition)
  put_str(m, "Datacenter", e.datacenter)
  put_str(m, "Service", e.service)
  put_str(m, "Redirect", e.redirect)
  match e.failover {
    Some(f) => m["Failover"] = query_datacenter_options_to_json(f)
    None => ()
  }
  @json.to_json(m)
}

///|
/// A `ServiceResolverConfigEntry` from a JSON object.
fn resolver_entry_of(m : Map[String, Json]) -> ServiceResolverConfigEntry {
  {
    name: jstr(m, "Name"),
    ns: jstr_opt(m, "Namespace"),
    partition: jstr_opt(m, "Partition"),
    datacenter: jstr_opt(m, "Datacenter"),
    service: jstr_opt(m, "Service"),
    failover: match m.get("Failover") {
      Some(f) =>
        if f is Object(_) {
          Some(query_datacenter_options_of(f))
        } else {
          None
        }
      None => None
    },
    redirect: jstr_opt(m, "Redirect"),
  }
}

///|
/// A `MeshConfigEntry` as a JSON object.
fn mesh_entry_to_json(e : MeshConfigEntry) -> Json {
  let m = jobj()
  m["Kind"] = @json.to_json("mesh")
  put_str(m, "Name", Some(e.name))
  put_str(m, "Partition", e.partition)
  match e.transparent_proxy {
    Some(t) => {
      let tm = jobj()
      put_str(tm, "Mode", t.mode)
      m["TransparentProxy"] = @json.to_json(tm)
    }
    None => ()
  }
  @json.to_json(m)
}

///|
/// A `MeshConfigEntry` from a JSON object.
fn mesh_entry_of(m : Map[String, Json]) -> MeshConfigEntry {
  {
    name: jstr(m, "Name"),
    partition: jstr_opt(m, "Partition"),
    transparent_proxy: match m.get("TransparentProxy") {
      Some(t) =>
        if t is Object(tm) {
          Some({ mode: jstr_opt(tm, "Mode"), })
        } else {
          None
        }
      None => None
    },
  }
}

///|
/// A `ConfigEntry` as its write body (with its `Kind`).
fn config_entry_to_json(entry : ConfigEntry) -> Json {
  match entry {
    ServiceDefault(e) => default_entry_to_json(e)
    ServiceRouter(e) => router_entry_to_json(e)
    ServiceSplitter(e) => splitter_entry_to_json(e)
    ServiceResolver(e) => resolver_entry_to_json(e)
    Mesh(e) => mesh_entry_to_json(e)
  }
}

///|
/// A `ConfigEntry` from its JSON object, dispatched on `Kind`.
fn config_entry_of(j : Json) -> ConfigEntry {
  let m = json_map(j)
  match jstr(m, "Kind") {
    "service-router" => ServiceRouter(router_entry_of(m))
    "service-splitter" => ServiceSplitter(splitter_entry_of(m))
    "service-resolver" => ServiceResolver(resolver_entry_of(m))
    "mesh" => Mesh(mesh_entry_of(m))
    _ => ServiceDefault(default_entry_of(m))
  }
}

///|
/// Read one config entry by `kind` and `name`.
pub async fn ConfigEntries::get(
  self : ConfigEntries,
  kind : String,
  name : String,
  options~ : QueryOptions = default_query_options,
) -> (ConfigEntry, QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get(
    "/v1/config/" + kind + "/" + name,
    options,
    [],
  )
  (config_entry_of(json), meta)
}

///|
/// List the config entries of `kind`.
pub async fn ConfigEntries::list(
  self : ConfigEntries,
  kind : String,
  options~ : QueryOptions = default_query_options,
) -> (Array[ConfigEntry], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/config/" + kind, options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[ConfigEntry] = []
  for item in json_array(json) {
    out.push(config_entry_of(item))
  }
  (out, meta)
}

///|
/// Write `entry`; the wrapped entry carries its own kind and name.
pub async fn ConfigEntries::set(
  self : ConfigEntries,
  entry : ConfigEntry,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let _ = config_entry_kind(entry)
  let body = @utf8.encode(config_entry_to_json(entry).stringify())
  let (_, meta) = self.client.do_put("/v1/config", body, options, [])
  meta
}

///|
/// Delete the config entry of `kind` and `name`.
pub async fn ConfigEntries::delete(
  self : ConfigEntries,
  kind : String,
  name : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_delete(
    "/v1/config/" + kind + "/" + name,
    options,
    [],
  )
  meta
}