///|
/// A handle to the Session API: sessions are ephemeral leases, bound to a set of
/// checks, that back locks and semaphores. `moonconsul/concurrency` keeps one
/// alive. When a session's checks fail or its
/// TTL lapses, its held keys are released.
pub struct Session {
  client : Client
}

///|
/// What happens to a session's held keys when it is invalidated: `Release`
/// frees them, `Delete` removes them.
pub(all) enum SessionBehavior {
  Release
  Delete
}

///|
/// A session: its `id` (assigned by Consul), the `checks` that keep it alive,
/// the `lock_delay` before a key can be locked again after the session dies, in
/// nanoseconds (Consul reads a value under 1000 as seconds), the `behavior` on
/// invalidation, and the `ttl` that bounds its life without a renewal.
pub(all) struct SessionEntry {
  id : String?
  name : String?
  node : String?
  checks : Array[String]?
  lock_delay : Int64?
  behavior : SessionBehavior?
  ttl : String?
}

///|
/// The default session: no id, the default checks, and Consul's default lock
/// delay and TTL.
pub let default_session_entry : SessionEntry = {
  id: None,
  name: None,
  node: None,
  checks: None,
  lock_delay: None,
  behavior: None,
  ttl: None,
}

///|
/// A `SessionEntry` from its JSON object.
fn session_entry_of(j : Json) -> SessionEntry {
  let m = json_map(j)
  {
    id: jstr_opt(m, "ID"),
    name: jstr_opt(m, "Name"),
    node: jstr_opt(m, "Node"),
    checks: jstr_array_opt(m, "Checks"),
    lock_delay: ji64_opt(m, "LockDelay"),
    behavior: match jstr_opt(m, "Behavior") {
      Some("delete") => Some(Delete)
      Some("release") => Some(Release)
      _ => None
    },
    ttl: jstr_opt(m, "TTL"),
  }
}

///|
/// The JSON write body for a `SessionEntry`; the id is assigned by Consul.
fn session_entry_to_json(e : SessionEntry) -> Json {
  let m = jobj()
  put_str(m, "Name", e.name)
  put_str(m, "Node", e.node)
  put_str_array(m, "Checks", e.checks)
  put_i64(m, "LockDelay", e.lock_delay)
  match e.behavior {
    Some(Release) => m["Behavior"] = @json.to_json("release")
    Some(Delete) => m["Behavior"] = @json.to_json("delete")
    None => ()
  }
  put_str(m, "TTL", e.ttl)
  @json.to_json(m)
}

///|
/// Create a session from `entry`; the returned entry carries the assigned id.
pub async fn Session::create(
  self : Session,
  entry~ : SessionEntry = default_session_entry,
  options~ : WriteOptions = default_write_options,
) -> (SessionEntry, WriteMeta) raise Refused {
  let body = @utf8.encode(session_entry_to_json(entry).stringify())
  let (json, meta) = self.client.do_put("/v1/session/create", body, options, [])
  let m = json_map(json)
  ({ ..entry, id: jstr_opt(m, "ID"), }, meta)
}

///|
/// Destroy `id`, releasing everything it holds.
pub async fn Session::destroy(
  self : Session,
  id : String,
  options~ : WriteOptions = default_write_options,
) -> WriteMeta raise Refused {
  let (_, meta) = self.client.do_put(
    "/v1/session/destroy/" + id,
    b"",
    options,
    [],
  )
  meta
}

///|
/// Renew `id`, returning the refreshed entry (or `None` if it expired).
pub async fn Session::renew(
  self : Session,
  id : String,
  options~ : WriteOptions = default_write_options,
) -> (SessionEntry?, WriteMeta) raise Refused {
  let (json, meta) = self.client.do_put(
    "/v1/session/renew/" + id,
    b"",
    options,
    [],
  ) catch {
    Refused::NotFound => return (None, empty_write_meta())
    e => raise e
  }
  let arr = json_array(json)
  match arr.get(0) {
    Some(item) => (Some(session_entry_of(item)), meta)
    None => (None, meta)
  }
}

///|
/// Read one session by `id`.
pub async fn Session::info(
  self : Session,
  id : String,
  options~ : QueryOptions = default_query_options,
) -> (SessionEntry?, QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/session/info/" + id, options, []) catch {
    Refused::NotFound => return (None, empty_query_meta())
    e => raise e
  }
  let arr = json_array(json)
  match arr.get(0) {
    Some(item) => (Some(session_entry_of(item)), meta)
    None => (None, meta)
  }
}

///|
/// The sessions active on `node`.
pub async fn Session::node(
  self : Session,
  node : String,
  options~ : QueryOptions = default_query_options,
) -> (Array[SessionEntry], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/session/node/" + node, options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[SessionEntry] = []
  for item in json_array(json) {
    out.push(session_entry_of(item))
  }
  (out, meta)
}

///|
/// Every session in the datacenter.
pub async fn Session::list(
  self : Session,
  options~ : QueryOptions = default_query_options,
) -> (Array[SessionEntry], QueryMeta) raise Refused {
  let (json, meta) = self.client.do_get("/v1/session/list", options, []) catch {
    Refused::NotFound => return ([], empty_query_meta())
    e => raise e
  }
  let out : Array[SessionEntry] = []
  for item in json_array(json) {
    out.push(session_entry_of(item))
  }
  (out, meta)
}