///|
/// 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
}

///|
/// The content type a `/metrics` scrape carries so a Prometheus server parses the
/// body as the text exposition format (`version=0.0.4`).
pub let exposition_content_type : String = "text/plain; version=0.0.4; charset=utf-8"

///|
/// Escape a label value for the text exposition format: backslash, double quote,
/// and newline are backslash-escaped; every other byte passes through unchanged.
fn escape_label(v : String) -> String {
  let sb = StringBuilder()
  for i = 0; i < v.length(); i = i + 1 {
    let c = v[i]
    if c == '\\' {
      sb.write_string("\\\\")
    } else if c == '"' {
      sb.write_string("\\\"")
    } else if c == '\n' {
      sb.write_string("\\n")
    } else {
      sb.write_char(c.unsafe_to_char())
    }
  }
  sb.to_string()
}

///|
/// Render a `{k="v",...}` label set (empty string when there are no labels), each
/// value escaped per the exposition spec.
fn labelset(pairs : Array[(String, String)]) -> String {
  if pairs.length() == 0 {
    return ""
  }
  let sb = StringBuilder()
  sb.write_char('{')
  for i = 0; i < pairs.length(); i = i + 1 {
    if i > 0 {
      sb.write_char(',')
    }
    sb.write_string(pairs[i].0)
    sb.write_string("=\"")
    sb.write_string(escape_label(pairs[i].1))
    sb.write_char('"')
  }
  sb.write_char('}')
  sb.to_string()
}

///|
/// Render this counter vector as Prometheus text exposition: a `# HELP` line, a
/// `# TYPE  counter` line, then one `{