// The configuration a connection and the client around it take: which server and how to
// authenticate, which RESP version to speak, and the pool, retry and client-side caching
// policies. Each policy is a record with a public preset of the market-standard defaults,
// so a caller overrides only what it must.

///|
/// Which RESP version to negotiate with `HELLO`. RESP2 is the long-standing wire; RESP3
/// adds the typed null, boolean, double, map, set and push values and is required for
/// client-side caching.
pub(all) enum Protocol {
  Resp2
  Resp3
}

///|
/// The `HELLO` version argument each protocol negotiates.
pub fn protocol_version(protocol : Protocol) -> Int32 {
  match protocol {
    Resp2 => 2
    Resp3 => 3
  }
}

///|
/// The pool a client draws connections from: its size bounds, how long an idle
/// connection lives, and whether a borrowed connection is checked before use.
pub(all) struct PoolOptions {
  /// The most idle connections kept ready; the pool drops idle ones past it. 10, as in
  /// `go-redis`'s `MaxIdle`.
  max_idle : Int32
  /// The most connections open at once; 0 means no cap, as `go-redis` defaults
  /// (`PoolSize`/`MaxActive` 0).
  max_active : Int32
  /// The fewest connections kept warm, so a burst does not open them all at once. 1, as
  /// `go-redis`'s `MinIdleConns`.
  min_idle : Int32
  /// How long an idle connection is kept before reclaim, in milliseconds. 300000 (5
  /// minutes), as `go-redis`'s `IdleTimeout`.
  idle_timeout : Int64
  /// Whether a connection is pinged before it is handed out. True, as `go-redis`'s
  /// `PoolFIFO` off and a live check on borrow.
  health_check : Bool
}

///|
pub let pool_default : PoolOptions = PoolOptions::{
  max_idle: 10,
  max_active: 0,
  min_idle: 1,
  idle_timeout: 300000,
  health_check: true,
}

///|
/// How a call retries after a transport failure: how many attempts in total, the backoff
/// between them and how hard it backs off.
pub(all) struct RetryOptions {
  /// Total attempts including the first; 3, as `go-redis`'s `MaxRetries`.
  attempts : Int32
  /// The delay before the first retry, in milliseconds. 100, as `go-redis`'s
  /// `MinRetryBackoff`/`MaxRetryBackoff` meet in the middle.
  base_delay : Int64
  /// The longest a retry waits, in milliseconds. 2000, as `go-redis`'s
  /// `MaxRetryBackoff`.
  max_delay : Int64
  /// The 0-9 aggressiveness of the backoff growth; each step waits `base * level^step`.
  /// 1 (linear), as `go-redis`'s near-linear backoff.
  level : Int32
  /// Whether to scatter retries by up to a quarter of the delay, so many clients do not
  /// retry in lockstep. True.
  jitter : Bool
}

///|
pub let retry_default : RetryOptions = RetryOptions::{
  attempts: 3,
  base_delay: 100,
  max_delay: 2000,
  level: 1,
  jitter: true,
}

///|
/// Client-side caching (`CLIENT TRACKING`): whether keys this client reads are tracked so
/// redis pushes invalidations, and in which mode.
pub(all) struct TrackingOptions {
  /// Whether tracking is on. False; it is off until a caller opts in.
  enabled : Bool
  /// Broadcast mode, where redis tracks and invalidates by prefix rather than per client
  /// table. False.
  bcast : Bool
  /// Opt-in tracking: only keys read inside a `CLIENT CACHING yes` block are tracked.
  /// False.
  optin : Bool
  /// Opt-out tracking: every read is tracked unless opted out. False.
  optout : Bool
  /// Suppress the invalidation echoed back to the client that caused it. False.
  noloop : Bool
  /// Track to another client's table by its ID (the redirect target pattern), rather
  /// than this connection's. None.
  redirect : Int64?
}

///|
pub let tracking_default : TrackingOptions = TrackingOptions::{
  enabled: false,
  bcast: false,
  optin: false,
  optout: false,
  noloop: false,
  redirect: None,
}

///|
/// Everything needed to reach and speak to a redis: the endpoint and credentials, the
/// protocol, the timeouts, and the pool, retry and tracking policies.
pub(all) struct ClientOptions {
  /// The host to dial. `127.0.0.1`.
  host : String
  /// The port to dial; 6379, redis's well-known port (not the organisation's 12000,
  /// which is for the servers this project serves).
  port : Int32
  /// The ACL username, when redis runs with ACL accounts. None (the default user).
  username : String?
  /// The password, for AUTH. None.
  password : String?
  /// The logical database to `SELECT` on connect. 0.
  db : Int32
  /// The RESP version to negotiate. Resp3.
  protocol : Protocol
  /// The TLS client config, when the connection is encrypted. None (cleartext).
  tls : @tls.Client?
  /// How long to wait for a connection to open, in milliseconds. 5000.
  connect_timeout : Int64
  /// How long to wait for a reply, in milliseconds. 0 means no bound.
  read_timeout : Int64
  /// How long to wait to enqueue a write, in milliseconds. 0 means no bound.
  write_timeout : Int64
  /// The pool policy.
  pool : PoolOptions
  /// The retry policy.
  retry : RetryOptions
  /// The client-side caching policy.
  tracking : TrackingOptions
}

///|
pub let client_default : ClientOptions = ClientOptions::{
  host: "127.0.0.1",
  port: 6379,
  username: None,
  password: None,
  db: 0,
  protocol: Resp3,
  tls: None,
  connect_timeout: 5000,
  read_timeout: 0,
  write_timeout: 0,
  pool: pool_default,
  retry: retry_default,
  tracking: tracking_default,
}

///|
/// `ClientOptions` at `host:port`, over the standard defaults (RESP3, database 0, no TLS).
pub fn ClientOptions::at(host : String, port : Int32) -> ClientOptions {
  { ..client_default, host, port, }
}

///|
/// A copy of `self` that authenticates with `username` and `password`.
pub fn ClientOptions::with_auth(
  self : ClientOptions,
  username : String?,
  password : String?,
) -> ClientOptions {
  { ..self, username, password, }
}

///|
/// A copy of `self` that `SELECT`s `db` on connect.
pub fn ClientOptions::with_db(
  self : ClientOptions,
  db : Int32,
) -> ClientOptions {
  { ..self, db, }
}

///|
/// The delay before retry number `attempt` (0-based), growing as `base * level^attempt`
/// and capped at `max_delay`. The transport adds jitter when the policy asks for it.
pub fn backoff_delay(attempt : Int32, retry : RetryOptions) -> Int64 {
  let step = if attempt < 0 { 0 } else { attempt }
  let level = if retry.level <= 0 { 1 } else { retry.level }
  let factor = ipow(level, step)
  let raw = retry.base_delay * factor
  if raw < 0 || raw > retry.max_delay {
    retry.max_delay
  } else {
    raw
  }
}

///|
fn ipow(base : Int32, exp : Int32) -> Int64 {
  if exp <= 0 {
    return 1
  }
  let mut result : Int64 = base.to_int64()
  for _i = 1; _i < exp; _i = _i + 1 {
    result = result * base.to_int64()
    if result > 9223372036854775807 {
      return 9223372036854775807
    }
  }
  result
}