///|
/// Online reliability and uncertainty calibration utilities.
///
/// Calibrated probabilities and conformal-style intervals are useful at the
/// final serving boundary: they turn a raw score into an operational signal
/// with explicit coverage, confidence, and alerting statistics.
pub struct ReliabilityBin {
  lower : Double
  upper : Double
  mut count : Int
  mut predicted_sum : Double
  mut observed_sum : Double
  mut squared_error : Double
}

///|
pub fn ReliabilityBin::new(lower : Double, upper : Double) -> ReliabilityBin {
  {
    lower,
    upper,
    count: 0,
    predicted_sum: 0.0,
    observed_sum: 0.0,
    squared_error: 0.0,
  }
}

///|
pub fn ReliabilityBin::lower(self : ReliabilityBin) -> Double {
  self.lower
}

///|
pub fn ReliabilityBin::upper(self : ReliabilityBin) -> Double {
  self.upper
}

///|
pub fn ReliabilityBin::count(self : ReliabilityBin) -> Int {
  self.count
}

///|
pub fn ReliabilityBin::mean_prediction(self : ReliabilityBin) -> Double {
  if self.count == 0 {
    0.0
  } else {
    self.predicted_sum / self.count.to_double()
  }
}

///|
pub fn ReliabilityBin::empirical_rate(self : ReliabilityBin) -> Double {
  if self.count == 0 {
    0.0
  } else {
    self.observed_sum / self.count.to_double()
  }
}

///|
pub fn ReliabilityBin::error(self : ReliabilityBin) -> Double {
  (self.mean_prediction() - self.empirical_rate()).abs()
}

///|
pub fn ReliabilityBin::brier(self : ReliabilityBin) -> Double {
  if self.count == 0 {
    0.0
  } else {
    self.squared_error / self.count.to_double()
  }
}

///|
pub fn ReliabilityBin::observe(
  self : ReliabilityBin,
  probability : Double,
  label : Double,
) -> Unit {
  self.count += 1
  self.predicted_sum += probability
  self.observed_sum += label
  let difference = probability - label
  self.squared_error += difference * difference
}

///|
pub fn ReliabilityBin::reset(self : ReliabilityBin) -> Unit {
  self.count = 0
  self.predicted_sum = 0.0
  self.observed_sum = 0.0
  self.squared_error = 0.0
}

///|
pub struct OnlineReliabilityMonitor {
  bins : Array[ReliabilityBin]
  mut observations : Int
  mut positives : Int
  mut log_loss : Double
}

///|
pub fn OnlineReliabilityMonitor::new(
  bin_count? : Int = 10,
) -> OnlineReliabilityMonitor {
  let count = if bin_count < 1 { 1 } else { bin_count }
  {
    bins: Array::makei(count, i => {
      ReliabilityBin::new(
        i.to_double() / count.to_double(),
        (i + 1).to_double() / count.to_double(),
      )
    }),
    observations: 0,
    positives: 0,
    log_loss: 0.0,
  }
}

///|
fn reliability_index(
  monitor : OnlineReliabilityMonitor,
  probability : Double,
) -> Int {
  let value = clamp_probability(probability)
  let index = (value * monitor.bins.length().to_double()).to_int()
  index.min(monitor.bins.length() - 1).max(0)
}

///|
pub fn OnlineReliabilityMonitor::observe(
  self : OnlineReliabilityMonitor,
  probability : Double,
  label : Double,
) -> Bool {
  if probability.is_nan() ||
    probability.is_inf() ||
    label.is_nan() ||
    label.is_inf() {
    false
  } else {
    let score = clamp_probability(probability)
    let target = clamp(label, 0.0, 1.0)
    self.bins[reliability_index(self, score)].observe(score, target)
    self.observations += 1
    if target >= 0.5 {
      self.positives += 1
    }
    let safe = clamp(score, 1.0e-12, 1.0 - 1.0e-12)
    self.log_loss += -(target * @math.ln(safe) +
      (1.0 - target) * @math.ln(1.0 - safe))
    true
  }
}

///|
pub fn OnlineReliabilityMonitor::bins(
  self : OnlineReliabilityMonitor,
) -> Array[ReliabilityBin] {
  self.bins.map(value => value)
}

///|
pub fn OnlineReliabilityMonitor::observations(
  self : OnlineReliabilityMonitor,
) -> Int {
  self.observations
}

///|
pub fn OnlineReliabilityMonitor::positive_rate(
  self : OnlineReliabilityMonitor,
) -> Double {
  if self.observations == 0 {
    0.0
  } else {
    self.positives.to_double() / self.observations.to_double()
  }
}

///|
pub fn OnlineReliabilityMonitor::log_loss(
  self : OnlineReliabilityMonitor,
) -> Double {
  if self.observations == 0 {
    0.0
  } else {
    self.log_loss / self.observations.to_double()
  }
}

///|
pub fn OnlineReliabilityMonitor::brier_score(
  self : OnlineReliabilityMonitor,
) -> Double {
  if self.observations == 0 {
    0.0
  } else {
    let total = Ref(0.0)
    for bin in self.bins {
      total.val += bin.squared_error
    }
    total.val / self.observations.to_double()
  }
}

///|
pub fn OnlineReliabilityMonitor::expected_calibration_error(
  self : OnlineReliabilityMonitor,
) -> Double {
  if self.observations == 0 {
    0.0
  } else {
    let total = Ref(0.0)
    for bin in self.bins {
      total.val += bin.count.to_double() * bin.error()
    }
    total.val / self.observations.to_double()
  }
}

///|
pub fn OnlineReliabilityMonitor::maximum_calibration_error(
  self : OnlineReliabilityMonitor,
) -> Double {
  let maximum = Ref(0.0)
  for bin in self.bins {
    if bin.error() > maximum.val {
      maximum.val = bin.error()
    }
  }
  maximum.val
}

///|
pub fn OnlineReliabilityMonitor::reset(self : OnlineReliabilityMonitor) -> Unit {
  for bin in self.bins {
    bin.reset()
  }
  self.observations = 0
  self.positives = 0
  self.log_loss = 0.0
}

///|
pub struct CalibratedInterval {
  center : Double
  radius : Double
  confidence : Double
}

///|
pub fn CalibratedInterval::new(
  center : Double,
  radius : Double,
  confidence : Double,
) -> CalibratedInterval {
  {
    center,
    radius: if radius < 0.0 {
      0.0
    } else {
      radius
    },
    confidence: clamp_probability(confidence),
  }
}

///|
pub fn CalibratedInterval::center(self : CalibratedInterval) -> Double {
  self.center
}

///|
pub fn CalibratedInterval::radius(self : CalibratedInterval) -> Double {
  self.radius
}

///|
pub fn CalibratedInterval::confidence(self : CalibratedInterval) -> Double {
  self.confidence
}

///|
pub fn CalibratedInterval::lower(self : CalibratedInterval) -> Double {
  self.center - self.radius
}

///|
pub fn CalibratedInterval::upper(self : CalibratedInterval) -> Double {
  self.center + self.radius
}

///|
pub fn CalibratedInterval::contains(
  self : CalibratedInterval,
  value : Double,
) -> Bool {
  value >= self.lower() && value <= self.upper()
}

///|
pub struct OnlineConformalRegressor {
  confidence : Double
  scores : OnlineQuantileSketch
  mut observations : Int
  mut covered : Int
  mut misses : Int
}

///|
pub fn OnlineConformalRegressor::new(
  confidence? : Double = 0.9,
  capacity? : Int = 512,
) -> OnlineConformalRegressor {
  {
    confidence: clamp(confidence, 0.0, 1.0),
    scores: OnlineQuantileSketch::new(capacity~),
    observations: 0,
    covered: 0,
    misses: 0,
  }
}

///|
pub fn OnlineConformalRegressor::observe(
  self : OnlineConformalRegressor,
  prediction : Double,
  actual : Double,
) -> Double {
  let score = (actual - prediction).abs()
  if !score.is_nan() && !score.is_inf() {
    self.scores.update(score)
  }
  self.observations += 1
  let interval = self.interval(prediction)
  if interval.contains(actual) {
    self.covered += 1
  } else {
    self.misses += 1
  }
  score
}

///|
pub fn OnlineConformalRegressor::interval(
  self : OnlineConformalRegressor,
  prediction : Double,
) -> CalibratedInterval {
  let probability = clamp(self.confidence, 0.0, 1.0)
  let radius = self.scores.quantile(probability).unwrap_or(0.0)
  CalibratedInterval::new(prediction, radius, self.confidence)
}

///|
pub fn OnlineConformalRegressor::confidence(
  self : OnlineConformalRegressor,
) -> Double {
  self.confidence
}

///|
pub fn OnlineConformalRegressor::radius(
  self : OnlineConformalRegressor,
) -> Double {
  self.scores.quantile(self.confidence).unwrap_or(0.0)
}

///|
pub fn OnlineConformalRegressor::observations(
  self : OnlineConformalRegressor,
) -> Int {
  self.observations
}

///|
pub fn OnlineConformalRegressor::covered(
  self : OnlineConformalRegressor,
) -> Int {
  self.covered
}

///|
pub fn OnlineConformalRegressor::misses(self : OnlineConformalRegressor) -> Int {
  self.misses
}

///|
pub fn OnlineConformalRegressor::coverage(
  self : OnlineConformalRegressor,
) -> Double {
  if self.observations == 0 {
    0.0
  } else {
    self.covered.to_double() / self.observations.to_double()
  }
}

///|
pub fn OnlineConformalRegressor::reset(self : OnlineConformalRegressor) -> Unit {
  self.scores.reset()
  self.observations = 0
  self.covered = 0
  self.misses = 0
}

///|
pub struct ProbabilityThresholdPolicy {
  target_rate : Double
  mut threshold : Double
  mut observations : Int
  mut positives : Int
}

///|
pub fn ProbabilityThresholdPolicy::new(
  target_rate? : Double = 0.1,
) -> ProbabilityThresholdPolicy {
  {
    target_rate: clamp(target_rate, 0.0, 1.0),
    threshold: 0.5,
    observations: 0,
    positives: 0,
  }
}

///|
pub fn ProbabilityThresholdPolicy::observe(
  self : ProbabilityThresholdPolicy,
  probability : Double,
) -> Bool {
  if probability.is_nan() || probability.is_inf() {
    false
  } else {
    let value = clamp_probability(probability)
    self.observations += 1
    if value >= self.threshold {
      self.positives += 1
    }
    let desired = 1.0 - self.target_rate
    if self.observations >= 10 {
      self.threshold = clamp(
        self.threshold +
        (
          self.positives.to_double() / self.observations.to_double() -
          self.target_rate
        ) *
        0.05,
        0.01,
        0.99,
      )
    }
    value >= desired
  }
}

///|
pub fn ProbabilityThresholdPolicy::threshold(
  self : ProbabilityThresholdPolicy,
) -> Double {
  self.threshold
}

///|
pub fn ProbabilityThresholdPolicy::target_rate(
  self : ProbabilityThresholdPolicy,
) -> Double {
  self.target_rate
}

///|
pub fn ProbabilityThresholdPolicy::observations(
  self : ProbabilityThresholdPolicy,
) -> Int {
  self.observations
}

///|
pub fn ProbabilityThresholdPolicy::positive_rate(
  self : ProbabilityThresholdPolicy,
) -> Double {
  if self.observations == 0 {
    0.0
  } else {
    self.positives.to_double() / self.observations.to_double()
  }
}

///|
pub fn ProbabilityThresholdPolicy::reset(
  self : ProbabilityThresholdPolicy,
) -> Unit {
  self.threshold = 0.5
  self.observations = 0
  self.positives = 0
}