// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// Which of several pools to one service to take from: two drawn by `first`
/// and `second`, each in `[0, 1)`, and of those the one whose breaker refuses
/// less, then the one with fewer in use.
///
/// Two random choices spread load almost as evenly as scanning every pool, and
/// cost two draws instead of a walk over all of them. `-1` when there are no pools.
pub fn[T] pick(
  pools : ArrayView[Pool[T]],
  now : Int64,
  first~ : Double,
  second~ : Double,
) -> Int32 {
  let count = pools.length()
  if count == 0 {
    return -1
  }
  let a = slot(first, count)
  let b = slot(second, count)
  let (ra, rb) = (pools[a].refusal(now), pools[b].refusal(now))
  if ra != rb {
    return if ra < rb { a } else { b }
  }
  if pools[b].busy() < pools[a].busy() {
    b
  } else {
    a
  }
}

///|
/// The share of new connections the ward's breaker is refusing at `now`; zero
/// without a breaker.
pub fn[T] Pool::refusal(self : Pool[T], now : Int64) -> Double {
  match self.ward.breaker {
    Some(breaker) => breaker.ratio(now)
    None => 0.0
  }
}

///|
fn slot(draw : Double, count : Int32) -> Int32 {
  let at = (draw * count.to_double()).to_int()
  if at < 0 {
    0
  } else if at >= count {
    count - 1
  } else {
    at
  }
}

///|
/// Takes turns. It holds only a cursor, so it costs nothing to keep one per client.
pub struct RoundRobin {
  priv mut cursor : Int32
}

///|
/// A rotation starting at the first target.
pub fn RoundRobin::new() -> RoundRobin {
  { cursor: 0, }
}

///|
/// The next of `count` targets, or `-1` when there are none. The cursor wraps at the current
/// count, so it stays valid as targets come and go.
pub fn RoundRobin::pick(self : RoundRobin, count : Int32) -> Int32 {
  if count <= 0 {
    return -1
  }
  let at = self.cursor % count
  self.cursor = (at + 1) % count
  at
}

///|
/// Smooth weighted round-robin, nginx's: every pick credits each target with its weight, serves
/// the one with the most credit, and charges it the total. Over a cycle each target gets its
/// share, and the turns interleave: a weight of 5 is not five requests back to back.
///
/// Credit is kept by name, so the list can change order between picks. A target that leaves
/// loses its credit, which was earned under weights that may no longer hold.
pub struct Weighted {
  priv scores : Map[String, Int32]
}

///|
/// A balancer with no credit yet.
pub fn Weighted::new() -> Weighted {
  { scores: Map([]), }
}

///|
/// The index of the next of `targets`, each a name and a weight. A weight of zero or less is
/// never picked, and `-1` means nothing can be.
pub fn Weighted::pick(
  self : Weighted,
  targets : ArrayView[(String, Int32)],
) -> Int32 {
  let mut best = -1
  let mut best_score = 0
  let mut total = 0
  let live : Map[String, Bool] = Map([])
  for at, target in targets {
    let (name, weight) = target
    if weight <= 0 {
      continue
    }
    live[name] = true
    total += weight
    let score = self.scores.get(name).unwrap_or(0) + weight
    self.scores[name] = score
    if best < 0 || score > best_score {
      best = at
      best_score = score
    }
  }
  for name in self.scores.keys().to_array() {
    if !live.contains(name) {
      self.scores.remove(name)
    }
  }
  if best >= 0 {
    self.scores[targets[best].0] = best_score - total
  }
  best
}