///|
// Load balancing. A `Balancer` holds the small amount of state repeated picks
// need (a round-robin cursor and the smooth-weighted-roller's per-backend
// credits) and chooses one healthy backend index. Randomness is injected for
// the `Random` policy, so every algorithm is deterministic under test. The
// native runtime keeps one Balancer per pool and calls `pick` per request.

///|
/// Per-pool balancer state.
pub struct Balancer {
  policy : LBPolicy
  mut cursor : Int
  mut current : Array[Int]
}

///|
/// Build a balancer for the given policy.
pub fn Balancer::new(policy? : LBPolicy = RoundRobin) -> Balancer {
  { policy, cursor: 0, current: [], }
}

///|
/// FNV-1a (32-bit) over a key's codepoints — a stable, dependency-free hash
/// used to pin a client to a backend. Keys here are IP addresses (ASCII), so
/// codepoint hashing and byte hashing agree. The offset basis exceeds the
/// signed Int range, so it is built as a UInt by reinterpreting its negative
/// two's-complement pattern.
fn fnv1a(key : String) -> UInt {
  let mut h = (-2128831035).reinterpret_as_uint() // FNV offset basis 2166136261
  let prime = (16777619).reinterpret_as_uint()
  for c in key {
    h = h ^ c.to_int().reinterpret_as_uint()
    h = h * prime
  }
  h
}

///|
/// Make sure the weighted-roller credit array matches the backend count,
/// resetting it when the pool resized so stale credits never leak.
fn ensure_current(balancer : Balancer, n : Int) -> Unit {
  if balancer.current.length() != n {
    let zeros : Array[Int] = []
    let mut i = 0
    while i < n {
      zeros.push(0)
      i = i + 1
    }
    balancer.current = zeros
  }
}

///|
/// Round-robin pick over the healthy subset.
fn pick_round_robin(balancer : Balancer, healthy : Array[Int]) -> Int {
  let n = healthy.length()
  let choice = healthy[balancer.cursor % n]
  balancer.cursor = balancer.cursor + 1
  choice
}

///|
/// Smooth weighted round-robin (nginx algorithm): each pass credits every
/// healthy backend its weight, picks the highest, then charges it the total —
/// spreading a weight-5 backend through the cycle instead of five in a row.
fn pick_weighted(
  balancer : Balancer,
  backends : Array[Backend],
  healthy : Array[Int],
) -> Int {
  ensure_current(balancer, backends.length())
  let mut total = 0
  for i in healthy {
    balancer.current[i] = balancer.current[i] + backends[i].weight
    total = total + backends[i].weight
  }
  let mut best = healthy[0]
  for i in healthy {
    if balancer.current[i] > balancer.current[best] {
      best = i
    }
  }
  balancer.current[best] = balancer.current[best] - total
  best
}

///|
/// Least-connections pick: the healthy backend with the fewest in-flight
/// requests; ties resolve to the first such backend.
fn pick_least_conn(backends : Array[Backend], healthy : Array[Int]) -> Int {
  let mut best = healthy[0]
  for i in healthy {
    if backends[i].active_conns < backends[best].active_conns {
      best = i
    }
  }
  best
}

///|
/// Choose a healthy backend index, or `None` when nothing is healthy (the
/// runtime then fails over or answers 502).
///
/// `client_key` feeds IPHash; `rand`, given `(n)` and returning a value in
/// `[0,n)`, feeds Random. If a policy's input is missing the choice falls back
/// to round-robin rather than failing.
pub fn pick(
  balancer : Balancer,
  backends : Array[Backend],
  client_key? : String = "",
  rand? : (Int) -> Int,
  exclude? : Array[Int] = [],
) -> Int? {
  let all_healthy = healthy_indices(backends)
  let healthy : Array[Int] = []
  for idx in all_healthy {
    if !exclude.contains(idx) {
      healthy.push(idx)
    }
  }
  if healthy.length() == 0 {
    return None
  }
  match balancer.policy {
    RoundRobin => Some(pick_round_robin(balancer, healthy))
    WeightedRR => Some(pick_weighted(balancer, backends, healthy))
    LeastConn => Some(pick_least_conn(backends, healthy))
    Random =>
      match rand {
        Some(r) => Some(healthy[r(healthy.length())])
        None => Some(pick_round_robin(balancer, healthy))
      }
    IPHash =>
      if client_key.length() == 0 {
        Some(pick_round_robin(balancer, healthy))
      } else {
        let n = healthy.length().reinterpret_as_uint()
        let slot = (fnv1a(client_key) % n).reinterpret_as_int()
        Some(healthy[slot])
      }
  }
}