///|
fn is_ascii_letter(char : Char) -> Bool {
  match char {
    'a'..='z' | 'A'..='Z' => true
    _ => false
  }
}

///|
fn is_ascii_digit(char : Char) -> Bool {
  match char {
    '0'..='9' => true
    _ => false
  }
}

///|
fn is_valid_identifier(value : String, allow_colon : Bool) -> Bool {
  if value.is_empty() {
    return false
  }
  let mut index = 0
  for char in value {
    let valid = if index == 0 {
      is_ascii_letter(char) || char == '_' || (allow_colon && char == ':')
    } else {
      is_ascii_letter(char) ||
      is_ascii_digit(char) ||
      char == '_' ||
      (allow_colon && char == ':')
    }
    if !valid {
      return false
    }
    index += 1
  }
  true
}

///|
fn validate_family_name(name : String) -> Unit raise EncodingError {
  if !is_valid_identifier(name, true) {
    raise InvalidSnapshot("invalid metric family name: \{name}")
  }
}

///|
fn validate_label_name(name : String) -> Unit raise EncodingError {
  if !is_valid_identifier(name, false) || name.has_prefix("__") {
    raise InvalidSnapshot("invalid label name: \{name}")
  }
}

///|
fn validate_unit(unit : String) -> Unit raise EncodingError {
  if !is_valid_identifier(unit, true) {
    raise InvalidSnapshot("invalid unit: \{unit}")
  }
}

///|
/// Compare text by Unicode scalar order, not MoonBit shortlex string ordering.
/// Prometheus tooling conventionally presents label and family names
/// lexicographically, so a one-character-longer name must not jump ahead just
/// because it is shorter.
fn lexicographic_compare(left : String, right : String) -> Int {
  let left_chars = [ for char in left => char ]
  let right_chars = [ for char in right => char ]
  let shared_length = if left_chars.length() < right_chars.length() {
    left_chars.length()
  } else {
    right_chars.length()
  }
  for index in 0.. right_chars.length() {
    1
  } else {
    0
  }
}

///|
fn sorted_labels(
  labels : Array[@metric.Label],
) -> Array[@metric.Label] raise EncodingError {
  let sorted = labels.copy()
  for index in 0.. 0 &&
          lexicographic_compare(sorted[position - 1].name, current.name) > 0 {
      sorted[position] = sorted[position - 1]
      position -= 1
    }
    sorted[position] = current
  }
  for index in 1.. String raise EncodingError {
  let output = StringBuilder()
  for label in sorted_labels(labels) {
    output.write_string("\{label.name.length()}:\{label.name}")
    output.write_string("\{label.value.length()}:\{label.value}")
  }
  output.to_string()
}

///|
fn write_labels(
  output : StringBuilder,
  labels : Array[@metric.Label],
  extra? : @metric.Label,
) -> Unit raise EncodingError {
  let sorted = sorted_labels(labels)
  match extra {
    Some(label) => {
      validate_label_name(label.name)
      for existing in sorted {
        if existing.name == label.name {
          raise DuplicateLabelName(label.name)
        }
      }
    }
    None => ()
  }
  if sorted.is_empty() && extra is None {
    return
  }
  output.write_char('{')
  for index, label in sorted {
    if index > 0 {
      output.write_char(',')
    }
    output.write_string(label.name)
    output.write_string("=\"")
    output.write_string(escape_label_value(label.value))
    output.write_char('"')
  }
  match extra {
    Some(label) => {
      if !sorted.is_empty() {
        output.write_char(',')
      }
      output.write_string(label.name)
      output.write_string("=\"")
      output.write_string(escape_label_value(label.value))
      output.write_char('"')
    }
    None => ()
  }
  output.write_char('}')
}

///|
fn write_metadata(
  output : StringBuilder,
  name : String,
  help : String,
  metric_type : String,
  unit : String?,
  format : ExpositionFormat,
) -> Unit raise EncodingError {
  validate_family_name(name)
  if !help.is_empty() {
    output.write_string("# HELP \{name} \{escape_help(help)}\n")
  }
  output.write_string("# TYPE \{name} \{metric_type}\n")
  if is_openmetrics(format) {
    match unit {
      Some(value) => {
        validate_unit(value)
        if !name.has_suffix("_" + value) {
          raise InvalidSnapshot(
            "OpenMetrics unit '\{value}' must be an underscore suffix of family '\{name}'",
          )
        }
        output.write_string("# UNIT \{name} \{value}\n")
      }
      None => ()
    }
  }
}

///|
/// Return the metadata name for one metric family in the selected wire format.
///
/// OpenMetrics 1.0 models a Counter family independently from its Total sample:
/// `# TYPE http_requests counter` is paired with `http_requests_total 1`.
/// Prometheus text retains the traditional single `http_requests_total` name
/// for both metadata and sample. Rejecting a missing suffix is preferable to
/// emitting a syntactically plausible but invalid OpenMetrics Counter.
fn metadata_name(
  name : String,
  family_type : @registry.FamilyType,
  format : ExpositionFormat,
) -> String raise EncodingError {
  if format != OpenMetricsText || family_type != CounterFamily {
    return name
  }
  match name.strip_suffix("_total") {
    Some(base) => {
      let result = base.to_owned()
      if result.is_empty() {
        raise InvalidSnapshot(
          "OpenMetrics counter name must have a base before _total: \{name}",
        )
      }
      result
    }
    None =>
      raise InvalidSnapshot(
        "OpenMetrics counter sample name must end in _total: \{name}",
      )
  }
}

///|
fn write_scalar_sample(
  output : StringBuilder,
  name : String,
  labels : Array[@metric.Label],
  value : Double,
  format : ExpositionFormat,
) -> Unit raise EncodingError {
  output.write_string(name)
  write_labels(output, labels)
  output.write_char(' ')
  let rendered = if is_openmetrics(format) {
    format_openmetrics_number(value)
  } else {
    format_number(value)
  }
  output.write_string(rendered)
  output.write_char('\n')
}

///|
fn write_histogram_sample(
  output : StringBuilder,
  name : String,
  labels : Array[@metric.Label],
  value : @metric.HistogramSnapshot,
  format : ExpositionFormat,
) -> Unit raise EncodingError {
  let boundaries = value.boundaries
  let counts = value.cumulative_counts
  if boundaries.length() != counts.length() {
    raise InvalidHistogram("boundary/count length mismatch for \{name}")
  }
  let mut previous_boundary : Double? = None
  let mut previous_count = 0L
  for index, boundary in boundaries {
    if boundary.is_nan() || boundary.is_inf() {
      raise InvalidHistogram("non-finite boundary for \{name}")
    }
    match previous_boundary {
      Some(previous) =>
        if boundary <= previous {
          raise InvalidHistogram(
            "boundaries are not strictly increasing for \{name}",
          )
        }
      None => ()
    }
    if counts[index] < previous_count || counts[index] > value.count {
      raise InvalidHistogram("invalid cumulative bucket count for \{name}")
    }
    output.write_string("\{name}_bucket")
    write_labels(output, labels, extra={
      name: "le",
      value: if is_openmetrics(format) {
        format_openmetrics_number(boundary)
      } else {
        format_number(boundary)
      },
    })
    output.write_char(' ')
    output.write_string(format_count(counts[index]))
    output.write_char('\n')
    previous_boundary = Some(boundary)
    previous_count = counts[index]
  }
  output.write_string("\{name}_bucket")
  write_labels(output, labels, extra={ name: "le", value: "+Inf" })
  output.write_char(' ')
  output.write_string(format_count(value.count))
  output.write_char('\n')
  output.write_string("\{name}_sum")
  write_labels(output, labels)
  output.write_char(' ')
  if is_openmetrics(format) {
    output.write_string(format_openmetrics_number(value.sum))
  } else {
    output.write_string(format_number(value.sum))
  }
  output.write_char('\n')
  output.write_string("\{name}_count")
  write_labels(output, labels)
  output.write_char(' ')
  output.write_string(format_count(value.count))
  output.write_char('\n')
}

///|
fn write_family(
  output : StringBuilder,
  family : @registry.FamilySnapshot,
  format : ExpositionFormat,
) -> Unit raise EncodingError {
  match family.family_type {
    CounterFamily => {
      if !family.histogram_samples.is_empty() {
        raise InvalidSnapshot(
          "counter family contains histogram samples: \{family.name}",
        )
      }
      let metadata_name = metadata_name(family.name, CounterFamily, format)
      write_metadata(
        output,
        metadata_name,
        family.help,
        "counter",
        family.unit,
        format,
      )
      let samples = family.scalar_samples.copy()
      for index in 0.. 0 &&
              lexicographic_compare(
                label_sort_key(samples[position - 1].labels),
                current_key,
              ) >
              0 {
          samples[position] = samples[position - 1]
          position -= 1
        }
        samples[position] = current
      }
      for sample in samples {
        write_scalar_sample(
          output,
          family.name,
          sample.labels,
          sample.value,
          format,
        )
      }
    }
    GaugeFamily => {
      if !family.histogram_samples.is_empty() {
        raise InvalidSnapshot(
          "gauge family contains histogram samples: \{family.name}",
        )
      }
      let metadata_name = metadata_name(family.name, GaugeFamily, format)
      write_metadata(
        output,
        metadata_name,
        family.help,
        "gauge",
        family.unit,
        format,
      )
      let samples = family.scalar_samples.copy()
      for index in 0.. 0 &&
              lexicographic_compare(
                label_sort_key(samples[position - 1].labels),
                current_key,
              ) >
              0 {
          samples[position] = samples[position - 1]
          position -= 1
        }
        samples[position] = current
      }
      for sample in samples {
        write_scalar_sample(
          output,
          family.name,
          sample.labels,
          sample.value,
          format,
        )
      }
    }
    HistogramFamily => {
      if !family.scalar_samples.is_empty() {
        raise InvalidSnapshot(
          "histogram family contains scalar samples: \{family.name}",
        )
      }
      let metadata_name = metadata_name(family.name, HistogramFamily, format)
      write_metadata(
        output,
        metadata_name,
        family.help,
        "histogram",
        family.unit,
        format,
      )
      let samples = family.histogram_samples.copy()
      for index in 0.. 0 &&
              lexicographic_compare(
                label_sort_key(samples[position - 1].labels),
                current_key,
              ) >
              0 {
          samples[position] = samples[position - 1]
          position -= 1
        }
        samples[position] = current
      }
      for sample in samples {
        write_histogram_sample(
          output,
          family.name,
          sample.labels,
          sample.value,
          format,
        )
      }
    }
  }
}

///|
/// Encode an immutable gather snapshot in the requested text exposition format.
///
/// Family and sample order is canonicalized here, so equivalent snapshots
/// produce identical bytes regardless of collection order. Prometheus text
/// 0.0.4 omits `UNIT` and `# EOF`; OpenMetrics 1.0 includes both.
pub fn encode_snapshot(
  snapshot : @registry.GatherSnapshot,
  format : ExpositionFormat,
) -> EncodedExposition raise EncodingError {
  let families = snapshot.families.copy()
  for index in 0.. 0 &&
          lexicographic_compare(families[position - 1].name, current.name) > 0 {
      families[position] = families[position - 1]
      position -= 1
    }
    families[position] = current
  }
  for index in 1.. EncodedExposition raise EncodingError {
  let output = StringBuilder()
  for counter in registry.counters() {
    let metadata_name = metadata_name(counter.name(), CounterFamily, format)
    write_metadata(
      output,
      metadata_name,
      counter.help(),
      "counter",
      counter.unit(),
      format,
    )
    write_scalar_sample(
      output,
      counter.name(),
      counter.labels(),
      counter.get(),
      format,
    )
  }
  for gauge in registry.gauges() {
    let metadata_name = metadata_name(gauge.name(), GaugeFamily, format)
    write_metadata(
      output,
      metadata_name,
      gauge.help(),
      "gauge",
      gauge.unit(),
      format,
    )
    write_scalar_sample(
      output,
      gauge.name(),
      gauge.labels(),
      gauge.get(),
      format,
    )
  }
  for histogram in registry.histograms() {
    let metadata_name = metadata_name(histogram.name(), HistogramFamily, format)
    write_metadata(
      output,
      metadata_name,
      histogram.help(),
      "histogram",
      histogram.unit(),
      format,
    )
    write_histogram_sample(
      output,
      histogram.name(),
      histogram.labels(),
      histogram.snapshot(),
      format,
    )
  }
  if is_openmetrics(format) {
    output.write_string("# EOF\n")
  }
  { format, content_type: content_type(format), body: output.to_string() }
}

///|
/// Convenience OpenMetrics entry point for a complete registry snapshot.
///
/// Unlike the scalar-only M0 compatibility function `encode_registry`, this
/// gathers every registered collector, including vectors and custom adapters.
/// New HTTP/exporter code should select an explicit format and use
/// `encode_snapshot(registry.gather(), format)` so gather and encoding errors
/// can be handled separately.
pub fn encode(registry : @registry.Registry) -> String {
  try! encode_snapshot(registry.gather(), OpenMetricsText).body
}