///|
/// A handle to the ACL API: tokens (the credentials calls carry), policies
/// (rule sets), roles (policy groupings), auth methods and binding rules.
pub struct ACL {
  client : Client
}

///|
/// A link from a token to a policy, by id or name.
pub(all) struct ACLTokenPolicyLink {
  id : String?
  name : String?
}

///|
/// A link from a token to a role, by id or name.
pub(all) struct ACLTokenRoleLink {
  id : String?
  name : String?
}

///|
/// A token: its `accessor_id` (for reference) and `secret_id` (for calls), the
/// `policies` and `roles` it binds, whether it is `is_local` to one datacenter,
/// and its `description` and `hash`.
pub(all) struct ACLToken {
  id : String?
  accessor_id : String?
  secret_id : String?
  description : String?
  policies : Array[ACLTokenPolicyLink]?
  roles : Array[ACLTokenRoleLink]?
  is_local : Bool?
  ns : String?
  partition : String?
  hash : String?
  expiration_time : String?
}

///|
/// A policy: a `name` and its `rules` (HCL or JSON) plus its `description`.
pub(all) struct ACLPolicy {
  id : String?
  name : String?
  description : String?
  rules : String?
  datacenter : String?
  ns : String?
  partition : String?
  hash : String?
}

///|
/// A link from a role to a policy, by id or name.
pub(all) struct ACLRolePolicyLink {
  id : String?
  name : String?
}

///|
/// A role: a `name` grouping `policies`, plus its `description` and `hash`.
pub(all) struct ACLRole {
  id : String?
  name : String?
  description : String?
  policies : Array[ACLRolePolicyLink]?
  ns : String?
  partition : String?
  hash : String?
}

///|
/// An auth method: a `name` and `kind` (e.g. `kubernetes`, `oidc`), its
/// `config`, and its `description`.
pub(all) struct ACLAuthMethod {
  name : String?
  kind : String?
  description : String?
  config : Map[String, String]?
  ns : String?
  partition : String?
}

///|
/// A binding rule: selects identities from an auth method and maps them to a
/// `bind_type`/`bind_name` via `selector`.
pub(all) struct ACLBindingRule {
  id : String?
  description : String?
  auth_method : String?
  selector : String?
  bind_type : String?
  bind_name : String?
  ns : String?
  partition : String?
}

///|
/// Parameters for `token_login`.
pub(all) struct ACLLoginParams {
  auth_method : String?
  bearer_token : String?
  token_metadata : Map[String, String]?
}

///|
/// A token's id for path building: its accessor id, else its id.
fn token_id(t : ACLToken) -> String {
  match t.accessor_id {
    Some(a) => a
    None =>
      match t.id {
        Some(i) => i
        None => ""
      }
  }
}

///|
/// Token policy links as a JSON array.
fn token_policy_links_to_json(links : Array[ACLTokenPolicyLink]) -> Json {
  let arr : Array[Json] = []
  for l in links {
    let m = jobj()
    put_str(m, "ID", l.id)
    put_str(m, "Name", l.name)
    arr.push(@json.to_json(m))
  }
  @json.to_json(arr)
}

///|
/// Token role links as a JSON array.
fn token_role_links_to_json(links : Array[ACLTokenRoleLink]) -> Json {
  let arr : Array[Json] = []
  for l in links {
    let m = jobj()
    put_str(m, "ID", l.id)
    put_str(m, "Name", l.name)
    arr.push(@json.to_json(m))
  }
  @json.to_json(arr)
}

///|
/// An `ACLToken` as its write body.
fn acl_token_to_json(t : ACLToken) -> Json {
  let m = jobj()
  put_str(m, "ID", t.id)
  put_str(m, "AccessorID", t.accessor_id)
  put_str(m, "SecretID", t.secret_id)
  put_str(m, "Description", t.description)
  match t.policies {
    Some(ps) => m["Policies"] = token_policy_links_to_json(ps)
    None => ()
  }
  match t.roles {
    Some(rs) => m["Roles"] = token_role_links_to_json(rs)
    None => ()
  }
  put_bool(m, "Local", t.is_local)
  put_str(m, "Namespace", t.ns)
  put_str(m, "Partition", t.partition)
  put_str(m, "ExpirationTime", t.expiration_time)
  @json.to_json(m)
}

///|
/// An `ACLToken` from its JSON object.
fn acl_token_of(j : Json) -> ACLToken {
  let m = json_map(j)
  let policies : Array[ACLTokenPolicyLink] = []
  for item in jarray(m, "Policies") {
    let pm = json_map(item)
    policies.push({ id: jstr_opt(pm, "ID"), name: jstr_opt(pm, "Name"), })
  }
  let roles : Array[ACLTokenRoleLink] = []
  for item in jarray(m, "Roles") {
    let rm = json_map(item)
    roles.push({ id: jstr_opt(rm, "ID"), name: jstr_opt(rm, "Name"), })
  }
  {
    id: jstr_opt(m, "ID"),
    accessor_id: jstr_opt(m, "AccessorID"),
    secret_id: jstr_opt(m, "SecretID"),
    description: jstr_opt(m, "Description"),
    policies: if policies.length() == 0 {
      None
    } else {
      Some(policies)
    },
    roles: if roles.length() == 0 {
      None
    } else {
      Some(roles)
    },
    is_local: jbool_opt(m, "Local"),
    ns: jstr_opt(m, "Namespace"),
    partition: jstr_opt(m, "Partition"),
    hash: jstr_opt(m, "Hash"),
    expiration_time: jstr_opt(m, "ExpirationTime"),
  }
}

///|
/// An `ACLPolicy` as its write body.
fn acl_policy_to_json(p : ACLPolicy) -> Json {
  let m = jobj()
  put_str(m, "ID", p.id)
  put_str(m, "Name", p.name)
  put_str(m, "Description", p.description)
  put_str(m, "Rules", p.rules)
  put_str(m, "Datacenter", p.datacenter)
  put_str(m, "Namespace", p.ns)
  put_str(m, "Partition", p.partition)
  @json.to_json(m)
}

///|
/// An `ACLPolicy` from its JSON object.
fn acl_policy_of(j : Json) -> ACLPolicy {
  let m = json_map(j)
  {
    id: jstr_opt(m, "ID"),
    name: jstr_opt(m, "Name"),
    description: jstr_opt(m, "Description"),
    rules: jstr_opt(m, "Rules"),
    datacenter: jstr_opt(m, "Datacenter"),
    ns: jstr_opt(m, "Namespace"),
    partition: jstr_opt(m, "Partition"),
    hash: jstr_opt(m, "Hash"),
  }
}

///|
/// An `ACLRole` as its write body.
fn acl_role_to_json(r : ACLRole) -> Json {
  let m = jobj()
  put_str(m, "ID", r.id)
  put_str(m, "Name", r.name)
  put_str(m, "Description", r.description)
  match r.policies {
    Some(ps) => {
      let arr : Array[Json] = []
      for l in ps {
        let lm = jobj()
        put_str(lm, "ID", l.id)
        put_str(lm, "Name", l.name)
        arr.push(@json.to_json(lm))
      }
      m["Policies"] = @json.to_json(arr)
    }
    None => ()
  }
  put_str(m, "Namespace", r.ns)
  put_str(m, "Partition", r.partition)
  @json.to_json(m)
}

///|
/// An `ACLRole` from its JSON object.
fn acl_role_of(j : Json) -> ACLRole {
  let m = json_map(j)
  let policies : Array[ACLRolePolicyLink] = []
  for item in jarray(m, "Policies") {
    let pm = json_map(item)
    policies.push({ id: jstr_opt(pm, "ID"), name: jstr_opt(pm, "Name"), })
  }
  {
    id: jstr_opt(m, "ID"),
    name: jstr_opt(m, "Name"),
    description: jstr_opt(m, "Description"),
    policies: if policies.length() == 0 {
      None
    } else {
      Some(policies)
    },
    ns: jstr_opt(m, "Namespace"),
    partition: jstr_opt(m, "Partition"),
    hash: jstr_opt(m, "Hash"),
  }
}

///|
/// An `ACLAuthMethod` as its write body (the kind is carried as `Type`).
fn acl_auth_method_to_json(a : ACLAuthMethod) -> Json {
  let m = jobj()
  put_str(m, "Name", a.name)
  put_str(m, "Type", a.kind)
  put_str(m, "Description", a.description)
  match a.config {
    Some(cf) => m["Config"] = map_to_json(cf)
    None => ()
  }
  put_str(m, "Namespace", a.ns)
  put_str(m, "Partition", a.partition)
  @json.to_json(m)
}

///|
/// An `ACLAuthMethod` from its JSON object.
fn acl_auth_method_of(j : Json) -> ACLAuthMethod {
  let m = json_map(j)
  {
    name: jstr_opt(m, "Name"),
    kind: jstr_opt(m, "Type"),
    description: jstr_opt(m, "Description"),
    config: jmap_opt(m, "Config"),
    ns: jstr_opt(m, "Namespace"),
    partition: jstr_opt(m, "Partition"),
  }
}

///|
/// An `ACLBindingRule` as its write body.
fn acl_binding_rule_to_json(r : ACLBindingRule) -> Json {
  let m = jobj()
  put_str(m, "ID", r.id)
  put_str(m, "Description", r.description)
  put_str(m, "AuthMethod", r.auth_method)
  put_str(m, "Selector", r.selector)
  put_str(m, "BindType", r.bind_type)
  put_str(m, "BindName", r.bind_name)
  put_str(m, "Namespace", r.ns)
  put_str(m, "Partition", r.partition)
  @json.to_json(m)
}

///|
/// An `ACLBindingRule` from its JSON object.
fn acl_binding_rule_of(j : Json) -> ACLBindingRule {
  let m = json_map(j)
  {
    id: jstr_opt(m, "ID"),
    description: jstr_opt(m, "Description"),
    auth_method: jstr_opt(m, "AuthMethod"),
    selector: jstr_opt(m, "Selector"),
    bind_type: jstr_opt(m, "BindType"),
    bind_name: jstr_opt(m, "BindName"),
    ns: jstr_opt(m, "Namespace"),
    partition: jstr_opt(m, "Partition"),
  }
}

///|
/// An `ACLLoginParams` as its write body.
fn acl_login_params_to_json(p : ACLLoginParams) -> Json {
  let m = jobj()
  put_str(m, "AuthMethod", p.auth_method)
  put_str(m, "BearerToken", p.bearer_token)
  match p.token_metadata {
    Some(md) => m["Meta"] = map_to_json(md)
    None => ()
  }
  @json.to_json(m)
}

///|
/// Create a token.
pub async fn ACL::token_create(
  self : ACL,
  token : ACLToken,
  options~ : WriteOptions = default_write_options,
) -> (ACLToken, WriteMeta) raise Refused {
  let body = @utf8.encode(acl_token_to_json(token).stringify())
  let (json, meta) = self.client.do_put("/v1/acl/token", body, options, [])
  (acl_token_of(json), meta)
}

///|
/// Update a token.
pub async fn ACL::token_update(
  self : ACL,
  token : ACLToken,
  options~ : WriteOptions = default_write_options,
) -> (ACLToken, WriteMeta) raise Refused {
  let body = @utf8.encode(acl_token_to_json(token).stringify())
  let (json, meta) = self.client.do_put(
    "/v1/acl/token/" + token_id(token),
    body,
    options,
    [],
  )
  (acl_token_of(json), meta)
}

///|
/// Read a token by id or accessor id.
pub async fn ACL::token_read(
  self : ACL,
  id : String,
  options~ : QueryOptions = default_query_options,
) -> (ACLToken, QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/acl/token/" + id, options, [])
  (acl_token_of(json), meta)
}

///|
/// Read the token the client is authenticated with.
pub async fn ACL::token_read_self(
  self : ACL,
  options~ : QueryOptions = default_query_options,
) -> (ACLToken, QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/acl/token/self", options, [])
  (acl_token_of(json), meta)
}

///|
/// List tokens.
pub async fn ACL::token_list(
  self : ACL,
  options~ : QueryOptions = default_query_options,
) -> (Array[ACLToken], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/acl/tokens", options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[ACLToken] = []
  for item in json_array(json) {
    out.push(acl_token_of(item))
  }
  (out, meta)
}

///|
/// Clone a token, returning a new one with the same policies and roles.
pub async fn ACL::token_clone(
  self : ACL,
  id : String,
  options~ : WriteOptions = default_write_options,
) -> (ACLToken, WriteMeta) raise Refused {
  let (json, meta) = self.client.do_put(
    "/v1/acl/token/" + id + "/clone",
    b"",
    options,
    [],
  )
  (acl_token_of(json), meta)
}

///|
/// Delete a token by id or accessor id.
pub async fn ACL::token_delete(
  self : ACL,
  id : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_delete("/v1/acl/token/" + id, options, [])
  meta
}

///|
/// Exchange an auth-method `bearer_token` for a Consul token.
pub async fn ACL::token_login(
  self : ACL,
  params : ACLLoginParams,
  options~ : WriteOptions = default_write_options,
) -> (ACLToken, WriteMeta) raise Refused {
  let body = @utf8.encode(acl_login_params_to_json(params).stringify())
  let (json, meta) = self.client.do_post("/v1/acl/login", body, options, [])
  (acl_token_of(json), meta)
}

///|
/// Drop the token the client is authenticated with.
pub async fn ACL::token_logout(
  self : ACL,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_post("/v1/acl/logout", b"", options, [])
  meta
}

///|
/// Create a policy.
pub async fn ACL::policy_create(
  self : ACL,
  policy : ACLPolicy,
  options~ : WriteOptions = default_write_options,
) -> (ACLPolicy, WriteMeta) raise Refused {
  let body = @utf8.encode(acl_policy_to_json(policy).stringify())
  let (json, meta) = self.client.do_put("/v1/acl/policy", body, options, [])
  (acl_policy_of(json), meta)
}

///|
/// Update a policy.
pub async fn ACL::policy_update(
  self : ACL,
  policy : ACLPolicy,
  options~ : WriteOptions = default_write_options,
) -> (ACLPolicy, WriteMeta) raise Refused {
  let id = match policy.id {
    Some(i) => i
    None => raise Refused::Param("acl.policy_update: policy.id is required")
  }
  let body = @utf8.encode(acl_policy_to_json(policy).stringify())
  let (json, meta) = self.client.do_put("/v1/acl/policy/" + id, body, options, [])
  (acl_policy_of(json), meta)
}

///|
/// Read a policy by id or name.
pub async fn ACL::policy_read(
  self : ACL,
  id : String,
  options~ : QueryOptions = default_query_options,
) -> (ACLPolicy, QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/acl/policy/" + id, options, [])
  (acl_policy_of(json), meta)
}

///|
/// List policies.
pub async fn ACL::policy_list(
  self : ACL,
  options~ : QueryOptions = default_query_options,
) -> (Array[ACLPolicy], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/acl/policies", options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[ACLPolicy] = []
  for item in json_array(json) {
    out.push(acl_policy_of(item))
  }
  (out, meta)
}

///|
/// Delete a policy by id or name.
pub async fn ACL::policy_delete(
  self : ACL,
  id : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_delete("/v1/acl/policy/" + id, options, [])
  meta
}

///|
/// Create a role.
pub async fn ACL::role_create(
  self : ACL,
  role : ACLRole,
  options~ : WriteOptions = default_write_options,
) -> (ACLRole, WriteMeta) raise Refused {
  let body = @utf8.encode(acl_role_to_json(role).stringify())
  let (json, meta) = self.client.do_put("/v1/acl/role", body, options, [])
  (acl_role_of(json), meta)
}

///|
/// Update a role.
pub async fn ACL::role_update(
  self : ACL,
  role : ACLRole,
  options~ : WriteOptions = default_write_options,
) -> (ACLRole, WriteMeta) raise Refused {
  let id = match role.id {
    Some(i) => i
    None => raise Refused::Param("acl.role_update: role.id is required")
  }
  let body = @utf8.encode(acl_role_to_json(role).stringify())
  let (json, meta) = self.client.do_put("/v1/acl/role/" + id, body, options, [])
  (acl_role_of(json), meta)
}

///|
/// Read a role by id or name.
pub async fn ACL::role_read(
  self : ACL,
  id : String,
  options~ : QueryOptions = default_query_options,
) -> (ACLRole, QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/acl/role/" + id, options, [])
  (acl_role_of(json), meta)
}

///|
/// Read a role by name.
pub async fn ACL::role_read_by_name(
  self : ACL,
  name : String,
  options~ : QueryOptions = default_query_options,
) -> (ACLRole, QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/acl/role/name/" + name, options, [])
  (acl_role_of(json), meta)
}

///|
/// List roles.
pub async fn ACL::role_list(
  self : ACL,
  options~ : QueryOptions = default_query_options,
) -> (Array[ACLRole], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/acl/roles", options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[ACLRole] = []
  for item in json_array(json) {
    out.push(acl_role_of(item))
  }
  (out, meta)
}

///|
/// Delete a role by id or name.
pub async fn ACL::role_delete(
  self : ACL,
  id : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_delete("/v1/acl/role/" + id, options, [])
  meta
}

///|
/// Create an auth method.
pub async fn ACL::auth_method_create(
  self : ACL,
  entry : ACLAuthMethod,
  options~ : WriteOptions = default_write_options,
) -> (ACLAuthMethod, WriteMeta) raise Refused {
  let body = @utf8.encode(acl_auth_method_to_json(entry).stringify())
  let (json, meta) = self.client.do_put("/v1/acl/auth-method", body, options, [])
  (acl_auth_method_of(json), meta)
}

///|
/// Update an auth method.
pub async fn ACL::auth_method_update(
  self : ACL,
  entry : ACLAuthMethod,
  options~ : WriteOptions = default_write_options,
) -> (ACLAuthMethod, WriteMeta) raise Refused {
  let name = match entry.name {
    Some(n) => n
    None => raise Refused::Param("acl.auth_method_update: name is required")
  }
  let body = @utf8.encode(acl_auth_method_to_json(entry).stringify())
  let (json, meta) = self.client.do_put(
    "/v1/acl/auth-method/" + name,
    body,
    options,
    [],
  )
  (acl_auth_method_of(json), meta)
}

///|
/// Read an auth method by name.
pub async fn ACL::auth_method_read(
  self : ACL,
  name : String,
  options~ : QueryOptions = default_query_options,
) -> (ACLAuthMethod, QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get(
    "/v1/acl/auth-method/" + name,
    options,
    [],
  )
  (acl_auth_method_of(json), meta)
}

///|
/// List auth methods.
pub async fn ACL::auth_method_list(
  self : ACL,
  options~ : QueryOptions = default_query_options,
) -> (Array[ACLAuthMethod], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/acl/auth-methods", options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[ACLAuthMethod] = []
  for item in json_array(json) {
    out.push(acl_auth_method_of(item))
  }
  (out, meta)
}

///|
/// Delete an auth method by name.
pub async fn ACL::auth_method_delete(
  self : ACL,
  name : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_delete(
    "/v1/acl/auth-method/" + name,
    options,
    [],
  )
  meta
}

///|
/// Create a binding rule.
pub async fn ACL::binding_rule_create(
  self : ACL,
  rule : ACLBindingRule,
  options~ : WriteOptions = default_write_options,
) -> (ACLBindingRule, WriteMeta) raise Refused {
  let body = @utf8.encode(acl_binding_rule_to_json(rule).stringify())
  let (json, meta) = self.client.do_put("/v1/acl/binding-rule", body, options, [])
  (acl_binding_rule_of(json), meta)
}

///|
/// Update a binding rule.
pub async fn ACL::binding_rule_update(
  self : ACL,
  rule : ACLBindingRule,
  options~ : WriteOptions = default_write_options,
) -> (ACLBindingRule, WriteMeta) raise Refused {
  let id = match rule.id {
    Some(i) => i
    None => raise Refused::Param("acl.binding_rule_update: id is required")
  }
  let body = @utf8.encode(acl_binding_rule_to_json(rule).stringify())
  let (json, meta) = self.client.do_put(
    "/v1/acl/binding-rule/" + id,
    body,
    options,
    [],
  )
  (acl_binding_rule_of(json), meta)
}

///|
/// Read a binding rule by id.
pub async fn ACL::binding_rule_read(
  self : ACL,
  id : String,
  options~ : QueryOptions = default_query_options,
) -> (ACLBindingRule, QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/acl/binding-rule/" + id, options, [])
  (acl_binding_rule_of(json), meta)
}

///|
/// List binding rules.
pub async fn ACL::binding_rule_list(
  self : ACL,
  options~ : QueryOptions = default_query_options,
) -> (Array[ACLBindingRule], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/acl/binding-rules", options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[ACLBindingRule] = []
  for item in json_array(json) {
    out.push(acl_binding_rule_of(item))
  }
  (out, meta)
}

///|
/// Delete a binding rule by id.
pub async fn ACL::binding_rule_delete(
  self : ACL,
  id : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_delete("/v1/acl/binding-rule/" + id, options, [])
  meta
}