///|
/// Streaming histogram and shallow-tree models for bounded online learning.
///
/// These components are deliberately small enough for edge deployment while
/// retaining useful production behavior: bounded memory, incremental updates,
/// quantile queries, adaptive thresholds, and prediction dispersion.
pub struct HistogramBin {
  lower : Double
  upper : Double
  mut count : Double
  mut sum : Double
  mut square_sum : Double
  mut positive : Double
}

///|
pub fn HistogramBin::new(lower : Double, upper : Double) -> HistogramBin {
  { lower, upper, count: 0.0, sum: 0.0, square_sum: 0.0, positive: 0.0 }
}

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

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

///|
pub fn HistogramBin::count(self : HistogramBin) -> Double {
  self.count
}

///|
pub fn HistogramBin::sum(self : HistogramBin) -> Double {
  self.sum
}

///|
pub fn HistogramBin::mean(self : HistogramBin) -> Double {
  if self.count <= 0.0 {
    0.0
  } else {
    self.sum / self.count
  }
}

///|
pub fn HistogramBin::variance(self : HistogramBin) -> Double {
  if self.count <= 0.0 {
    0.0
  } else {
    let value = self.square_sum / self.count - self.mean() * self.mean()
    if value < 0.0 {
      0.0
    } else {
      value
    }
  }
}

///|
pub fn HistogramBin::positive_rate(self : HistogramBin) -> Double {
  if self.count <= 0.0 {
    0.0
  } else {
    self.positive / self.count
  }
}

///|
pub fn HistogramBin::observe(
  self : HistogramBin,
  value : Double,
  weight : Double,
  positive : Bool,
) -> Unit {
  let safe_weight = if weight <= 0.0 || weight.is_nan() || weight.is_inf() {
    0.0
  } else {
    weight
  }
  self.count += safe_weight
  self.sum += value * safe_weight
  self.square_sum += value * value * safe_weight
  if positive {
    self.positive += safe_weight
  }
}

///|
pub fn HistogramBin::reset(self : HistogramBin) -> Unit {
  self.count = 0.0
  self.sum = 0.0
  self.square_sum = 0.0
  self.positive = 0.0
}

///|
pub struct AdaptiveHistogram {
  lower : Double
  upper : Double
  bins : Array[HistogramBin]
  mut seen : Double
  mut outliers : Int
}

///|
pub fn AdaptiveHistogram::new(
  lower : Double,
  upper : Double,
  bin_count? : Int = 32,
) -> AdaptiveHistogram {
  let left = if lower.is_nan() || lower.is_inf() { 0.0 } else { lower }
  let right = if upper <= left || upper.is_nan() || upper.is_inf() {
    left + 1.0
  } else {
    upper
  }
  let count = if bin_count < 1 { 1 } else { bin_count }
  let width = (right - left) / count.to_double()
  {
    lower: left,
    upper: right,
    bins: Array::makei(count, i => {
      HistogramBin::new(
        left + i.to_double() * width,
        left + (i + 1).to_double() * width,
      )
    }),
    seen: 0.0,
    outliers: 0,
  }
}

///|
fn adaptive_histogram_index(
  histogram : AdaptiveHistogram,
  value : Double,
) -> Int {
  let width = (histogram.upper - histogram.lower) /
    histogram.bins.length().to_double()
  if value <= histogram.lower {
    0
  } else if value >= histogram.upper {
    histogram.bins.length() - 1
  } else {
    ((value - histogram.lower) / width)
    .to_int()
    .max(0)
    .min(histogram.bins.length() - 1)
  }
}

///|
pub fn AdaptiveHistogram::observe(
  self : AdaptiveHistogram,
  value : Double,
  weight? : Double = 1.0,
  positive? : Bool = false,
) -> Bool {
  if value.is_nan() || value.is_inf() {
    false
  } else {
    if value < self.lower || value > self.upper {
      self.outliers += 1
    }
    let index = adaptive_histogram_index(self, value)
    self.bins[index].observe(value, weight, positive)
    self.seen += if weight <= 0.0 { 0.0 } else { weight }
    true
  }
}

///|
pub fn AdaptiveHistogram::bin(
  self : AdaptiveHistogram,
  index : Int,
) -> HistogramBin? {
  self.bins.get(index)
}

///|
pub fn AdaptiveHistogram::bin_count(self : AdaptiveHistogram) -> Int {
  self.bins.length()
}

///|
pub fn AdaptiveHistogram::seen(self : AdaptiveHistogram) -> Double {
  self.seen
}

///|
pub fn AdaptiveHistogram::outliers(self : AdaptiveHistogram) -> Int {
  self.outliers
}

///|
pub fn AdaptiveHistogram::mean(self : AdaptiveHistogram) -> Double {
  if self.seen <= 0.0 {
    0.0
  } else {
    let total = Ref(0.0)
    for bin in self.bins {
      total.val += bin.sum()
    }
    total.val / self.seen
  }
}

///|
pub fn AdaptiveHistogram::variance(self : AdaptiveHistogram) -> Double {
  if self.seen <= 0.0 {
    0.0
  } else {
    let squares = Ref(0.0)
    for bin in self.bins {
      squares.val += bin.square_sum
    }
    let value = squares.val / self.seen - self.mean() * self.mean()
    if value < 0.0 {
      0.0
    } else {
      value
    }
  }
}

///|
pub fn AdaptiveHistogram::quantile(
  self : AdaptiveHistogram,
  probability : Double,
) -> Double {
  if self.seen <= 0.0 {
    0.0
  } else {
    let target = clamp(probability, 0.0, 1.0) * self.seen
    let running = Ref(0.0)
    let result = Ref(self.upper)
    for bin in self.bins {
      running.val += bin.count()
      if running.val >= target && result.val == self.upper {
        result.val = (bin.lower() + bin.upper()) / 2.0
      }
    }
    result.val
  }
}

///|
pub fn AdaptiveHistogram::cdf(
  self : AdaptiveHistogram,
  value : Double,
) -> Double {
  if self.seen <= 0.0 {
    0.0
  } else if value <= self.lower {
    0.0
  } else if value >= self.upper {
    1.0
  } else {
    let index = adaptive_histogram_index(self, value)
    let before = Ref(0.0)
    for i in 0.. Double {
  if self.seen <= 0.0 {
    0.0
  } else {
    let positives = Ref(0.0)
    for bin in self.bins {
      positives.val += bin.positive
    }
    positives.val / self.seen
  }
}

///|
pub fn AdaptiveHistogram::reset(self : AdaptiveHistogram) -> Unit {
  for bin in self.bins {
    bin.reset()
  }
  self.seen = 0.0
  self.outliers = 0
}

///|
pub struct StumpStatistics {
  mut count : Double
  mut sum : Double
  mut square_sum : Double
  mut positive : Double
}

///|
pub fn StumpStatistics::new() -> StumpStatistics {
  { count: 0.0, sum: 0.0, square_sum: 0.0, positive: 0.0 }
}

///|
pub fn StumpStatistics::observe(
  self : StumpStatistics,
  label : Double,
  positive : Bool,
  weight : Double,
) -> Unit {
  self.count += weight
  self.sum += label * weight
  self.square_sum += label * label * weight
  if positive {
    self.positive += weight
  }
}

///|
pub fn StumpStatistics::count(self : StumpStatistics) -> Double {
  self.count
}

///|
pub fn StumpStatistics::mean(self : StumpStatistics) -> Double {
  if self.count <= 0.0 {
    0.0
  } else {
    self.sum / self.count
  }
}

///|
pub fn StumpStatistics::variance(self : StumpStatistics) -> Double {
  if self.count <= 0.0 {
    0.0
  } else {
    let value = self.square_sum / self.count - self.mean() * self.mean()
    if value < 0.0 {
      0.0
    } else {
      value
    }
  }
}

///|
pub fn StumpStatistics::positive_rate(self : StumpStatistics) -> Double {
  if self.count <= 0.0 {
    0.0
  } else {
    self.positive / self.count
  }
}

///|
pub fn StumpStatistics::reset(self : StumpStatistics) -> Unit {
  self.count = 0.0
  self.sum = 0.0
  self.square_sum = 0.0
  self.positive = 0.0
}

///|
pub struct StreamingDecisionStump {
  feature : Int
  threshold : Double
  left : StumpStatistics
  right : StumpStatistics
  mut updates : Int
  mut mistakes : Int
}

///|
pub fn StreamingDecisionStump::new(
  feature : Int,
  threshold : Double,
) -> StreamingDecisionStump {
  {
    feature: if feature < 0 {
      0
    } else {
      feature
    },
    threshold,
    left: StumpStatistics::new(),
    right: StumpStatistics::new(),
    updates: 0,
    mistakes: 0,
  }
}

///|
pub fn StreamingDecisionStump::feature(self : StreamingDecisionStump) -> Int {
  self.feature
}

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

///|
pub fn StreamingDecisionStump::branch(
  self : StreamingDecisionStump,
  features : Array[Double],
) -> Bool {
  features.get(self.feature).unwrap_or(0.0) >= self.threshold
}

///|
pub fn StreamingDecisionStump::predict(
  self : StreamingDecisionStump,
  features : Array[Double],
) -> Double {
  if self.branch(features) {
    self.right.mean()
  } else {
    self.left.mean()
  }
}

///|
pub fn StreamingDecisionStump::predict_probability(
  self : StreamingDecisionStump,
  features : Array[Double],
) -> Double {
  if self.branch(features) {
    self.right.positive_rate()
  } else {
    self.left.positive_rate()
  }
}

///|
pub fn StreamingDecisionStump::update(
  self : StreamingDecisionStump,
  features : Array[Double],
  label : Double,
  weight? : Double = 1.0,
) -> Bool {
  let safe_weight = if weight <= 0.0 || weight.is_nan() { 0.0 } else { weight }
  let positive = label >= 0.5
  let branch = self.branch(features)
  let before = self.predict(features)
  if branch {
    self.right.observe(label, positive, safe_weight)
  } else {
    self.left.observe(label, positive, safe_weight)
  }
  self.updates += 1
  if (before >= 0.5) != positive {
    self.mistakes += 1
  }
  true
}

///|
pub fn StreamingDecisionStump::updates(self : StreamingDecisionStump) -> Int {
  self.updates
}

///|
pub fn StreamingDecisionStump::mistakes(self : StreamingDecisionStump) -> Int {
  self.mistakes
}

///|
pub fn StreamingDecisionStump::accuracy(
  self : StreamingDecisionStump,
) -> Double {
  if self.updates == 0 {
    0.0
  } else {
    1.0 - self.mistakes.to_double() / self.updates.to_double()
  }
}

///|
pub fn StreamingDecisionStump::left(
  self : StreamingDecisionStump,
) -> StumpStatistics {
  self.left
}

///|
pub fn StreamingDecisionStump::right(
  self : StreamingDecisionStump,
) -> StumpStatistics {
  self.right
}

///|
pub fn StreamingDecisionStump::reset(self : StreamingDecisionStump) -> Unit {
  self.left.reset()
  self.right.reset()
  self.updates = 0
  self.mistakes = 0
}

///|
pub struct StreamingStumpEnsemble {
  stumps : Array[StreamingDecisionStump]
  mut observations : Int
  mut total_loss : Double
}

///|
pub fn StreamingStumpEnsemble::new(
  features : Array[(Int, Double)],
) -> StreamingStumpEnsemble {
  let stumps = Array::make(0, StreamingDecisionStump::new(0, 0.0))
  for item in features {
    stumps.push(StreamingDecisionStump::new(item.0, item.1))
  }
  { stumps, observations: 0, total_loss: 0.0 }
}

///|
pub fn StreamingStumpEnsemble::predict(
  self : StreamingStumpEnsemble,
  features : Array[Double],
) -> Double {
  if self.stumps.is_empty() {
    0.0
  } else {
    let total = Ref(0.0)
    for stump in self.stumps {
      total.val += stump.predict(features)
    }
    total.val / self.stumps.length().to_double()
  }
}

///|
pub fn StreamingStumpEnsemble::predict_probability(
  self : StreamingStumpEnsemble,
  features : Array[Double],
) -> Double {
  if self.stumps.is_empty() {
    0.0
  } else {
    let total = Ref(0.0)
    for stump in self.stumps {
      total.val += stump.predict_probability(features)
    }
    total.val / self.stumps.length().to_double()
  }
}

///|
pub fn StreamingStumpEnsemble::dispersion(
  self : StreamingStumpEnsemble,
  features : Array[Double],
) -> Double {
  if self.stumps.length() <= 1 {
    0.0
  } else {
    let mean = self.predict(features)
    let total = Ref(0.0)
    for stump in self.stumps {
      let difference = stump.predict(features) - mean
      total.val += difference * difference
    }
    (total.val / self.stumps.length().to_double()).sqrt()
  }
}

///|
pub fn StreamingStumpEnsemble::update(
  self : StreamingStumpEnsemble,
  features : Array[Double],
  label : Double,
) -> Double {
  let prediction = self.predict(features)
  let error = label - prediction
  for stump in self.stumps {
    let _ = stump.update(features, label)
  }
  self.observations += 1
  self.total_loss += error * error
  prediction
}

///|
pub fn StreamingStumpEnsemble::stump_count(
  self : StreamingStumpEnsemble,
) -> Int {
  self.stumps.length()
}

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

///|
pub fn StreamingStumpEnsemble::mse(self : StreamingStumpEnsemble) -> Double {
  if self.observations == 0 {
    0.0
  } else {
    self.total_loss / self.observations.to_double()
  }
}

///|
pub fn StreamingStumpEnsemble::stumps(
  self : StreamingStumpEnsemble,
) -> Array[StreamingDecisionStump] {
  self.stumps.map(value => value)
}

///|
pub fn StreamingStumpEnsemble::reset(self : StreamingStumpEnsemble) -> Unit {
  for stump in self.stumps {
    stump.reset()
  }
  self.observations = 0
  self.total_loss = 0.0
}

///|
pub struct AdaptiveThreshold {
  target_rate : Double
  histogram : AdaptiveHistogram
  mut threshold : Double
  mut updates : Int
}

///|
pub fn AdaptiveThreshold::new(
  lower : Double,
  upper : Double,
  target_rate? : Double = 0.05,
  bins? : Int = 64,
) -> AdaptiveThreshold {
  {
    target_rate: clamp(target_rate, 0.0, 1.0),
    histogram: AdaptiveHistogram::new(lower, upper, bin_count=bins),
    threshold: upper,
    updates: 0,
  }
}

///|
pub fn AdaptiveThreshold::observe(
  self : AdaptiveThreshold,
  value : Double,
) -> Bool {
  let accepted = self.histogram.observe(value)
  if accepted {
    self.updates += 1
    self.threshold = self.histogram.quantile(1.0 - self.target_rate)
  }
  accepted
}

///|
pub fn AdaptiveThreshold::is_anomaly(
  self : AdaptiveThreshold,
  value : Double,
) -> Bool {
  value > self.threshold
}

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

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

///|
pub fn AdaptiveThreshold::updates(self : AdaptiveThreshold) -> Int {
  self.updates
}

///|
pub fn AdaptiveThreshold::score(
  self : AdaptiveThreshold,
  value : Double,
) -> Double {
  if self.threshold <= 0.0 {
    0.0
  } else {
    clamp(value / self.threshold, 0.0, 2.0)
  }
}

///|
pub fn AdaptiveThreshold::reset(self : AdaptiveThreshold) -> Unit {
  self.histogram.reset()
  self.threshold = self.histogram.upper
  self.updates = 0
}