///|
/// A deterministic in-memory metric registry.
///
/// The registry deliberately has no clock, threads, network or process-global
/// state. Callers supply labels and values, then snapshot or encode the result.
pub struct Registry {
  priv families : Array[MetricFamily]
}

///|
/// Create an empty registry.
pub fn Registry::new() -> Registry {
  { families: [] }
}

///|
fn Registry::family_index(self : Registry, name : StringView) -> Int {
  find_family_index(self.families, name)
}

///|
fn Registry::require_type(
  self : Registry,
  name : StringView,
  expected : MetricType,
) -> Result[Int, String] {
  let index = self.family_index(name)
  if index < 0 {
    return Err("metric family '\{name}' is not registered")
  }
  let actual = self.families[index].metric_type
  if actual != expected {
    return Err(
      "metric family '\{name}' has type \{actual.to_keyword()}, expected \{expected.to_keyword()}",
    )
  }
  Ok(index)
}

///|
/// Register metadata for a new metric family.
pub fn Registry::register(
  self : Registry,
  name : String,
  metric_type : MetricType,
  help? : String,
  unit? : String,
) -> Result[Unit, String] {
  if !is_valid_metric_name(name) {
    return Err("invalid metric family name '\{name}'")
  }
  if metric_type == Unknown {
    return Err("registry families require a concrete metric type")
  }
  if self.family_index(name) >= 0 {
    return Err("metric family '\{name}' is already registered")
  }
  self.families.push(MetricFamily::new(name, metric_type~, help?, unit?))
  Ok(())
}

///|
fn find_series_index(
  samples : Array[Sample],
  name : String,
  labels : Array[Label],
) -> Int {
  for index in 0.. MetricFamily {
  let samples = family.samples.copy()
  let index = find_series_index(samples, name, labels)
  if index < 0 {
    samples.push(Sample::new(name, value, labels~))
  } else {
    let current = samples[index]
    let next_value = if add { current.value + value } else { value }
    samples[index] = { ..current, value: next_value }
  }
  { ..family, samples, }
}

///|
/// Set or replace an arbitrary sample in a registered family.
///
/// This lower-level operation is useful for summary and info collectors.
pub fn Registry::set_sample(
  self : Registry,
  family_name : StringView,
  sample : Sample,
) -> Result[Unit, String] {
  let index = self.family_index(family_name)
  if index < 0 {
    return Err("metric family '\{family_name}' is not registered")
  }
  let family = self.families[index]
  if !sample_matches_family(family, sample.name) {
    return Err("sample '\{sample.name}' does not match family '\{family.name}'")
  }
  let samples = family.samples.copy()
  let sample_index = find_series_index(samples, sample.name, sample.labels)
  if sample_index < 0 {
    samples.push(sample)
  } else {
    samples[sample_index] = sample
  }
  self.families[index] = { ..family, samples, }
  Ok(())
}

///|
/// Increment a counter series. Negative deltas are rejected.
pub fn Registry::increment_counter(
  self : Registry,
  name : StringView,
  delta? : Double = 1.0,
  labels? : Array[Label] = [],
) -> Result[Unit, String] {
  if delta.is_nan() || delta < 0.0 {
    return Err("counter delta must be a non-negative number")
  }
  let index = match self.require_type(name, Counter) {
    Ok(index) => index
    Err(error) => return Err(error)
  }
  let family = self.families[index]
  self.families[index] = update_or_insert(
    family,
    "\{family.name}_total",
    labels,
    delta,
    true,
  )
  Ok(())
}

///|
/// Set a gauge series to an absolute value.
pub fn Registry::set_gauge(
  self : Registry,
  name : StringView,
  value : Double,
  labels? : Array[Label] = [],
) -> Result[Unit, String] {
  let index = match self.require_type(name, Gauge) {
    Ok(index) => index
    Err(error) => return Err(error)
  }
  let family = self.families[index]
  self.families[index] = update_or_insert(
    family,
    family.name,
    labels,
    value,
    false,
  )
  Ok(())
}

///|
/// Set an info series. Info samples always carry the value `1`.
pub fn Registry::set_info(
  self : Registry,
  name : StringView,
  labels? : Array[Label] = [],
) -> Result[Unit, String] {
  let index = match self.require_type(name, Info) {
    Ok(index) => index
    Err(error) => return Err(error)
  }
  let family = self.families[index]
  self.families[index] = update_or_insert(
    family,
    "\{family.name}_info",
    labels,
    1.0,
    false,
  )
  Ok(())
}

///|
/// Set one state in a state-set family.
pub fn Registry::set_state(
  self : Registry,
  name : StringView,
  state : String,
  enabled : Bool,
  labels? : Array[Label] = [],
) -> Result[Unit, String] {
  if state == "" {
    return Err("state name must not be empty")
  }
  let index = match self.require_type(name, StateSet) {
    Ok(index) => index
    Err(error) => return Err(error)
  }
  let family = self.families[index]
  if labels_have_name(labels, family.name) {
    return Err(
      "caller labels must not contain the reserved state label '\{family.name}'",
    )
  }
  let state_labels = labels.copy()
  state_labels.push(Label::new(family.name, state))
  self.families[index] = update_or_insert(
    family,
    family.name,
    state_labels,
    if enabled {
      1.0
    } else {
      0.0
    },
    false,
  )
  Ok(())
}

///|
fn clear_summary_group(
  family : MetricFamily,
  labels : Array[Label],
) -> MetricFamily {
  let samples : Array[Sample] = []
  for sample in family.samples {
    let belongs = if sample.name == family.name {
      same_label_group_ignoring(sample.labels, labels, "quantile")
    } else if sample.name == "\{family.name}_sum" ||
      sample.name == "\{family.name}_count" {
      same_label_set(sample.labels, labels)
    } else {
      false
    }
    if !belongs {
      samples.push(sample)
    }
  }
  { ..family, samples, }
}

///|
fn labels_with_quantile(
  labels : Array[Label],
  quantile : Double,
) -> Array[Label] {
  let result = labels.copy()
  result.push(Label::new("quantile", format_number(quantile)))
  result
}

///|
/// Replace one complete summary label group.
///
/// Quantile keys must be strictly increasing from zero to one, and observed
/// quantile values must be non-decreasing.
pub fn Registry::set_summary(
  self : Registry,
  name : StringView,
  quantiles : Array[(Double, Double)],
  sum : Double,
  count : Double,
  labels? : Array[Label] = [],
) -> Result[Unit, String] {
  if labels_have_name(labels, "quantile") {
    return Err("summary labels must not contain the reserved 'quantile' label")
  }
  if !is_nonnegative_integer(count) {
    return Err("summary count must be a non-negative integer")
  }
  if sum.is_nan() || sum < 0.0 {
    return Err("summary sum must be non-NaN and non-negative")
  }
  let mut previous_quantile = -1.0
  let mut previous_value = -@double.infinity
  for pair in quantiles {
    if pair.0.is_nan() || pair.0 < 0.0 || pair.0 > 1.0 {
      return Err("summary quantiles must be between 0 and 1")
    }
    if pair.0 <= previous_quantile {
      return Err("summary quantiles must be strictly increasing")
    }
    if pair.1 < 0.0 {
      return Err("summary values must not be negative")
    }
    if count == 0.0 && !pair.1.is_nan() {
      return Err("summary values must be NaN when count is zero")
    }
    if pair.1 < previous_value {
      return Err("summary values must not decrease across quantiles")
    }
    previous_quantile = pair.0
    previous_value = pair.1
  }
  let index = match self.require_type(name, Summary) {
    Ok(index) => index
    Err(error) => return Err(error)
  }
  let original = self.families[index]
  let mut family = clear_summary_group(original, labels)
  for pair in quantiles {
    family = update_or_insert(
      family,
      family.name,
      labels_with_quantile(labels, pair.0),
      pair.1,
      false,
    )
  }
  family = update_or_insert(family, "\{family.name}_sum", labels, sum, false)
  family = update_or_insert(
    family,
    "\{family.name}_count",
    labels,
    count,
    false,
  )
  self.families[index] = family
  Ok(())
}

///|
fn labels_with_bound(labels : Array[Label], bound : String) -> Array[Label] {
  let result = labels.copy()
  result.push(Label::new("le", bound))
  result
}

///|
fn clear_gaugehistogram_group(
  family : MetricFamily,
  labels : Array[Label],
) -> MetricFamily {
  let samples : Array[Sample] = []
  for sample in family.samples {
    let belongs = if sample.name == "\{family.name}_bucket" {
      same_label_group_ignoring(sample.labels, labels, "le")
    } else if sample.name == "\{family.name}_gsum" ||
      sample.name == "\{family.name}_gcount" {
      same_label_set(sample.labels, labels)
    } else {
      false
    }
    if !belongs {
      samples.push(sample)
    }
  }
  { ..family, samples, }
}

///|
/// Replace one complete gauge-histogram label group.
///
/// Each tuple contains a finite upper bound and its cumulative bucket count.
pub fn Registry::set_gauge_histogram(
  self : Registry,
  name : StringView,
  buckets : Array[(Double, Double)],
  sum : Double,
  count : Double,
  labels? : Array[Label] = [],
) -> Result[Unit, String] {
  if labels_have_name(labels, "le") {
    return Err(
      "gauge-histogram labels must not contain the reserved 'le' label",
    )
  }
  if !is_nonnegative_integer(count) {
    return Err("gauge-histogram count must be a non-negative integer")
  }
  if sum.is_nan() {
    return Err("gauge-histogram sum must not be NaN")
  }
  let mut previous_bound = -@double.infinity
  let mut previous_count = 0.0
  for bucket in buckets {
    if bucket.0.is_nan() || bucket.0 == @double.infinity {
      return Err("gauge-histogram finite bucket boundaries are required")
    }
    if bucket.0 <= previous_bound {
      return Err("gauge-histogram bounds must be strictly increasing")
    }
    if !is_nonnegative_integer(bucket.1) || bucket.1 < previous_count {
      return Err(
        "gauge-histogram cumulative counts must be non-negative integers and must not decrease",
      )
    }
    previous_bound = bucket.0
    previous_count = bucket.1
  }
  if previous_count > count {
    return Err("final finite bucket count must not exceed total count")
  }
  let index = match self.require_type(name, GaugeHistogram) {
    Ok(index) => index
    Err(error) => return Err(error)
  }
  let original = self.families[index]
  let mut family = clear_gaugehistogram_group(original, labels)
  for bucket in buckets {
    family = update_or_insert(
      family,
      "\{family.name}_bucket",
      labels_with_bound(labels, format_number(bucket.0)),
      bucket.1,
      false,
    )
  }
  family = update_or_insert(
    family,
    "\{family.name}_bucket",
    labels_with_bound(labels, "+Inf"),
    count,
    false,
  )
  family = update_or_insert(family, "\{family.name}_gsum", labels, sum, false)
  family = update_or_insert(
    family,
    "\{family.name}_gcount",
    labels,
    count,
    false,
  )
  self.families[index] = family
  Ok(())
}

///|
fn validate_buckets(buckets : Array[Double]) -> Result[Unit, String] {
  let mut previous = -@double.infinity
  for bound in buckets {
    if bound.is_nan() || bound == @double.infinity || bound < 0.0 {
      return Err(
        "non-negative finite histogram bucket boundaries are required because the registry exposes a sum",
      )
    }
    if bound <= previous {
      return Err("histogram bucket boundaries must be strictly increasing")
    }
    previous = bound
  }
  Ok(())
}

///|
fn validate_existing_histogram_schema(
  family : MetricFamily,
  labels : Array[Label],
  buckets : Array[Double],
) -> Result[Unit, String] {
  let bucket_name = "\{family.name}_bucket"
  let existing : Array[Double] = []
  let mut saw_bucket = false
  for sample in family.samples {
    if sample.name != bucket_name ||
      !same_label_group_ignoring(sample.labels, labels, "le") {
      continue
    }
    saw_bucket = true
    match label_number(sample, "le") {
      Some(bound) => if bound != @double.infinity { existing.push(bound) }
      None =>
        return Err(
          "existing histogram bucket schema contains an invalid boundary",
        )
    }
  }
  if !saw_bucket {
    return Ok(())
  }
  if existing.length() != buckets.length() {
    return Err(
      "histogram bucket boundaries must match the existing schema for this label set",
    )
  }
  for index in 0.. Result[Unit, String] {
  if labels_have_name(labels, "le") {
    return Err("histogram labels must not contain the reserved 'le' label")
  }
  if value.is_nan() || value < 0.0 {
    return Err(
      "histogram observations must be non-NaN and non-negative because the registry exposes a sum",
    )
  }
  match validate_buckets(buckets) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let index = match self.require_type(name, Histogram) {
    Ok(index) => index
    Err(error) => return Err(error)
  }
  let mut family = self.families[index]
  match validate_existing_histogram_schema(family, labels, buckets) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  for bound in buckets {
    if value <= bound {
      family = update_or_insert(
        family,
        "\{family.name}_bucket",
        labels_with_bound(labels, format_number(bound)),
        1.0,
        true,
      )
    } else {
      let bound_labels = labels_with_bound(labels, format_number(bound))
      if find_series_index(
          family.samples,
          "\{family.name}_bucket",
          bound_labels,
        ) <
        0 {
        family = update_or_insert(
          family,
          "\{family.name}_bucket",
          bound_labels,
          0.0,
          false,
        )
      }
    }
  }
  family = update_or_insert(
    family,
    "\{family.name}_bucket",
    labels_with_bound(labels, "+Inf"),
    1.0,
    true,
  )
  family = update_or_insert(family, "\{family.name}_sum", labels, value, true)
  family = update_or_insert(family, "\{family.name}_count", labels, 1.0, true)
  self.families[index] = family
  Ok(())
}

///|
/// Copy the current registry into an OpenMetrics document.
pub fn Registry::snapshot(self : Registry) -> Document {
  let families : Array[MetricFamily] = []
  for family in self.families {
    families.push({ ..family, samples: family.samples.copy() })
  }
  { families, has_eof: true }
}

///|
/// Remove all samples from a registered family while retaining its metadata.
pub fn Registry::reset_family(
  self : Registry,
  name : StringView,
) -> Result[Unit, String] {
  let index = self.family_index(name)
  if index < 0 {
    return Err("metric family '\{name}' is not registered")
  }
  let family = self.families[index]
  self.families[index] = { ..family, samples: [] }
  Ok(())
}

///|
/// Remove an entire family and return whether it existed.
pub fn Registry::remove_family(self : Registry, name : StringView) -> Bool {
  let index = self.family_index(name)
  if index < 0 {
    false
  } else {
    self.families.remove(index) |> ignore
    true
  }
}

///|
/// Encode a snapshot of the registry.
pub fn Registry::to_text(self : Registry) -> String {
  encode(self.snapshot())
}

///|
/// Return the number of registered families.
pub fn Registry::length(self : Registry) -> Int {
  self.families.length()
}