///|
/// A monotonic counter (← go-zero's `metric.CounterVec`) partitioned by a label
/// string. Each `inc`/`add` accrues against one label (e.g. `"GET /ping 200"`),
/// so a single vector holds the per-method/route/status request tallies Prometheus
/// scrapes. Counters only ever go up.
pub struct CounterVec {
  counts : Map[String, Int64]
}

///|
/// A fresh counter vector with no labels seen yet.
pub fn CounterVec::new() -> CounterVec {
  { counts: Map([]) }
}

///|
/// Add `delta` to `label`'s count (creating the series on first sight).
pub fn CounterVec::add(
  self : CounterVec,
  label : String,
  delta : Int64,
) -> Unit {
  self.counts[label] = self.value(label) + delta
}

///|
/// Increment `label`'s count by one.
pub fn CounterVec::inc(self : CounterVec, label : String) -> Unit {
  self.add(label, 1L)
}

///|
/// The current count for `label`, `0` if never touched.
pub fn CounterVec::value(self : CounterVec, label : String) -> Int64 {
  self.counts.get(label).unwrap_or(0L)
}

///|
/// The sum of every label's count — the total number of observations.
pub fn CounterVec::total(self : CounterVec) -> Int64 {
  let mut sum = 0L
  for _, v in self.counts {
    sum = sum + v
  }
  sum
}

///|
/// The set of labels that have been observed.
pub fn CounterVec::labels(self : CounterVec) -> Array[String] {
  self.counts.keys().collect()
}

///|
/// A cumulative histogram (← go-zero's `metric.HistogramVec`, Prometheus
/// semantics): a sorted list of `le` (less-than-or-equal) upper bounds and, for
/// each, the count of observations that fell at or below it, plus the running
/// `sum` and total `count`. An observation above every bound still lands in the
/// implicit `+Inf` bucket that `count` represents.
pub struct Histogram {
  bounds : Array[Double]
  bucket : Array[Int64]
  mut sum : Double
  mut count : Int64
}

///|
/// The default latency buckets go-zero ships (milliseconds): a request spends
/// most of its time under a second, so the bounds cluster there.
pub let default_latency_buckets : Array[Double] = [
  1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000,
]

///|
/// A histogram over `bounds` (defaulting to `default_latency_buckets`). The
/// bounds are taken as given; supply them in ascending order, as Prometheus
/// requires.
pub fn Histogram::new(
  bounds? : Array[Double] = default_latency_buckets,
) -> Histogram {
  { bounds, bucket: Array::make(bounds.length(), 0L), sum: 0.0, count: 0 }
}

///|
/// Record one observation: it lands in every bucket whose `le` bound it does not
/// exceed (cumulative), and updates the sum and count.
pub fn Histogram::observe(self : Histogram, value : Double) -> Unit {
  for i = 0; i < self.bounds.length(); i = i + 1 {
    if value <= self.bounds[i] {
      self.bucket[i] = self.bucket[i] + 1L
    }
  }
  self.sum = self.sum + value
  self.count = self.count + 1L
}

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

///|
/// The sum of all observed values (Prometheus `_sum`).
pub fn Histogram::sum_value(self : Histogram) -> Double {
  self.sum
}

///|
/// The cumulative count in the bucket bounded by `bounds[i]` — how many
/// observations were `<=` that bound.
pub fn Histogram::bucket_count(self : Histogram, i : Int) -> Int64 {
  self.bucket[i]
}

///|
/// The upper bounds this histogram partitions on.
pub fn Histogram::bounds(self : Histogram) -> Array[Double] {
  self.bounds
}

///|
/// The mean of the observations, or `0` when none have been recorded.
pub fn Histogram::mean(self : Histogram) -> Double {
  if self.count == 0L {
    0.0
  } else {
    self.sum / self.count.to_double()
  }
}

///|
/// The request metrics an HTTP service exposes (← go-zero's server metrics): a
/// request counter partitioned by method/route/status and a latency histogram.
/// Held by the caller so it can be read out for a `/metrics` scrape after serving.
pub struct ServerMetrics {
  requests : CounterVec
  latency : Histogram
}

///|
/// Fresh server metrics: an empty counter and a default-bucket latency histogram.
pub fn ServerMetrics::new() -> ServerMetrics {
  { requests: CounterVec::new(), latency: Histogram::new() }
}

///|
/// The request counter, labelled `"  "`.
pub fn ServerMetrics::requests(self : ServerMetrics) -> CounterVec {
  self.requests
}

///|
/// The request-latency histogram, in milliseconds.
pub fn ServerMetrics::latency(self : ServerMetrics) -> Histogram {
  self.latency
}

///|
/// Metrics middleware (← go-zero's `prometheus` interceptor): time each HTTP
/// request on the shared clock and, when the response starts, count it under
/// `"  "` and record its latency in milliseconds. The
/// record is taken once per request even if a downstream (under a recovery race)
/// emits a second start. Non-HTTP scopes pass through unmeasured.
pub fn metrics(m : ServerMetrics, clock : Clock) -> Middleware {
  inner => {
    (scope, receive, send) => {
      match scope {
        Http(hs) => {
          let start = clock.now()
          let verb = hs.http_method
          let path = hs.path
          let recorded : Ref[Bool] = { val: false }
          let observed : @moonasgi.Send = event => {
            match event {
              HttpResponseStart(status~, headers~, trailers~) => {
                if !recorded.val {
                  recorded.val = true
                  m.requests.inc(verb + " " + path + " " + status.to_string())
                  m.latency.observe((clock.now() - start).to_double())
                }
                send(
                  @moonasgi.Event::HttpResponseStart(
                    status~,
                    headers~,
                    trailers~,
                  ),
                )
              }
              other => send(other)
            }
          }
          inner(scope, receive, observed)
        }
        _ => inner(scope, receive, send)
      }
    }
  }
}