///|
// Active health probing — the scheduler half of the active/passive health
// system. Passive health reacts to real traffic (`health.mbt`); active probing
// reaches out to backends that are currently out of rotation so a recovered
// server is brought back even while no traffic would be sent to it.
//
// This file is pure logic and runs on every target: given the current logical
// tick (milliseconds) it decides which backends are due and records a probe
// result through the existing health thresholds. The native runtime owns the
// clock (a sleep loop) and the real dial attempt (`@socket.Tcp::connect`).

///|
/// How often a backend that is down is actively probed.
pub struct ProbePolicy {
  interval_ms : Int
}

///|
/// Build a probe policy; an interval below 1ms is treated as 1ms.
pub fn ProbePolicy::new(interval_ms? : Int = 1000) -> ProbePolicy {
  { interval_ms: if interval_ms < 1 { 1 } else { interval_ms }, }
}

///|
/// The configured probe interval in milliseconds.
pub fn ProbePolicy::interval_ms(self : ProbePolicy) -> Int {
  self.interval_ms
}

///|
/// Indices of backends due for an active probe at tick `now_ms`.
///
/// By default only unhealthy backends are probed (probing healthy ones is
/// redundant with passive health). A backend is due when it has never been
/// probed (`last_probe_ms == 0`) or at least one interval has elapsed.
pub fn due_backends(
  backends : Array[Backend],
  now_ms : Int,
  policy : ProbePolicy,
  only_unhealthy? : Bool = true,
) -> Array[Int] {
  let out : Array[Int] = []
  let mut i = 0
  while i < backends.length() {
    let b = backends[i]
    let eligible = if only_unhealthy { !b.healthy } else { true }
    let elapsed = now_ms - b.last_probe_ms
    let due = b.last_probe_ms == 0 || elapsed >= policy.interval_ms
    if eligible && due {
      out.push(i)
    }
    i = i + 1
  }
  out
}

///|
/// Record the outcome of one active probe at tick `now_ms`. The probe timestamp
/// is stamped regardless of the result, and the outcome is fed through the same
/// thresholds as passive traffic, so recovery still needs a full run of
/// successes (no flap from a single lucky dial).
pub fn record_probe(
  backends : Array[Backend],
  idx : Int,
  ok : Bool,
  health : HealthPolicy,
  now_ms : Int,
) -> Unit {
  let b = backends[idx]
  b.last_probe_ms = now_ms
  backends[idx] = b
  if ok {
    record_success(backends, idx, health)
  } else {
    record_failure(backends, idx, health)
  }
}