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

///|
/// The default bucket bounds the official Prometheus clients share: 0.005
/// through 10, fourteen finite bounds plus the implicit `+Inf` bucket every
/// histogram has.
pub let buckets : Array[Double] = [
  0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0,
]

///|
fn default_buckets() -> Array[Double] {
  [
    0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0,
  ]
}

///|
/// Bounds must be finite and strictly increasing; the implicit `+Inf` bucket is
/// added at encoding time, so passing infinity here is refused.
fn copy_bounds(bounds : Array[Double]) -> Array[Double] {
  let out : Array[Double] = []
  for b in bounds {
    out.push(b)
  }
  out
}

///|
fn check_buckets(bounds : Array[Double]) -> Unit raise Refused {
  if bounds.length() == 0 {
    raise Refused("a histogram needs at least one bucket")
  }
  let inf = 1.0 / 0.0
  let mut prev = 0.0 - inf
  let mut first = true
  for b in bounds {
    if b != b {
      raise Refused("bucket bound is NaN")
    }
    if b == inf || b == 0.0 - inf {
      raise Refused("bucket bounds must be finite")
    }
    if !first && b <= prev {
      raise Refused("bucket bounds must be strictly increasing")
    }
    prev = b
    first = false
  }
}

///|
/// `count` bounds starting at `start` and growing by `width`.
pub fn linear(
  start : Double,
  width : Double,
  count : Int32,
) -> Array[Double] raise Refused {
  if count <= 0 {
    raise Refused("linear needs a positive count, got " + count.to_string())
  }
  if width <= 0.0 {
    raise Refused("linear needs a positive width, got " + width.to_string())
  }
  let out : Array[Double] = []
  for i = 0; i < count; i = i + 1 {
    out.push(start + width * i.to_double())
  }
  out
}

///|
/// `count` bounds starting at `start` and multiplying by `factor`.
pub fn exponential(
  start : Double,
  factor : Double,
  count : Int32,
) -> Array[Double] raise Refused {
  if count <= 0 {
    raise Refused(
      "exponential needs a positive count, got " + count.to_string(),
    )
  }
  if start <= 0.0 {
    raise Refused(
      "exponential needs a positive start, got " + start.to_string(),
    )
  }
  if factor <= 0.0 {
    raise Refused(
      "exponential needs a positive factor, got " + factor.to_string(),
    )
  }
  let out : Array[Double] = []
  let mut v = start
  for i = 0; i < count; i = i + 1 {
    out.push(v)
    v = v * factor
  }
  out
}

///|
/// A histogram with cumulative buckets. Counts are kept per bucket and made
/// cumulative on snapshot, which is what the wire format carries.
pub struct Histogram {
  priv bounds : Array[Double]
  priv counts : Array[Int64]
  priv mut sum : Double
  priv mut count : Int64
  priv exemplars : Array[Exemplar?]
  priv created : Double?
}

///|
/// A histogram over `buckets` (the shared defaults unless given).
pub fn Histogram::new(
  buckets~ : Array[Double] = default_buckets(),
  created? : Double,
) -> Histogram raise Refused {
  check_buckets(buckets)
  let bounds = copy_bounds(buckets)
  let n = bounds.length() + 1
  {
    bounds,
    counts: Array::make(n, 0L),
    sum: 0.0,
    count: 0L,
    exemplars: Array::make(n, None),
    created,
  }
}

///|
/// Record one observation in the first bucket whose bound it does not exceed,
/// or in the `+Inf` bucket when it exceeds every bound.
pub fn Histogram::observe(
  self : Histogram,
  v : Double,
  exemplar? : Exemplar,
) -> Unit {
  self.sum = self.sum + v
  self.count = self.count + 1L
  let mut index = self.bounds.length()
  for i = 0; i < self.bounds.length(); i = i + 1 {
    if v <= self.bounds[i] {
      index = i
      break
    }
  }
  self.counts[index] = self.counts[index] + 1L
  match exemplar {
    Some(e) => self.exemplars[index] = Some(e)
    None => ()
  }
}

///|
/// The snapshot of this histogram: cumulative bucket counts, the sum, and the
/// total count, with the retained exemplars.
pub fn Histogram::snapshot(self : Histogram) -> Point {
  let cumulative : Array[(Double, Int64)] = []
  let mut acc = 0L
  for i = 0; i < self.bounds.length(); i = i + 1 {
    acc = acc + self.counts[i]
    cumulative.push((self.bounds[i], acc))
  }
  let exemplars : Array[Exemplar] = []
  for e in self.exemplars {
    match e {
      Some(x) => exemplars.push(x)
      None => ()
    }
  }
  {
    labels: [],
    value: Buckets(buckets=cumulative, sum=self.sum, count=self.count),
    timestamp: None,
    created: self.created,
    exemplars,
  }
}

///|
/// The number of observations.
pub fn Histogram::total(self : Histogram) -> Int64 {
  self.count
}

///|
/// The sum of the observations.
pub fn Histogram::sum_value(self : Histogram) -> Double {
  self.sum
}

///|
pub impl Metric for Histogram with fn kind(_self) {
  Kind::Histogram
}

///|
pub impl Metric for Histogram with fn points(self) {
  [self.snapshot()]
}

///|
pub extend Histogram with Metric::{kind, points}

///|
/// A gauge histogram: the same buckets as a histogram, but the sum and count are
/// a gauge's, and the wire format spells them `_gsum` and `_gcount`.
pub struct GaugeHistogram {
  priv inner : Histogram
}

///|
/// A gauge histogram over `buckets` (the shared defaults unless given).
pub fn GaugeHistogram::new(
  buckets~ : Array[Double] = default_buckets(),
  created? : Double,
) -> GaugeHistogram raise Refused {
  { inner: Histogram::new(buckets~, created?), }
}

///|
/// Record one observation.
pub fn GaugeHistogram::observe(
  self : GaugeHistogram,
  v : Double,
  exemplar? : Exemplar,
) -> Unit {
  self.inner.observe(v, exemplar?)
}

///|
/// The snapshot of this gauge histogram.
pub fn GaugeHistogram::snapshot(self : GaugeHistogram) -> Point {
  self.inner.snapshot()
}

///|
pub impl Metric for GaugeHistogram with fn kind(_self) {
  Kind::GaugeHistogram
}

///|
pub impl Metric for GaugeHistogram with fn points(self) {
  [self.snapshot()]
}

///|
pub extend GaugeHistogram with Metric::{kind, points}