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

///|
/// Which instrument a name belongs to.
///
/// The first four are written to as the program runs. The three `Observable` ones are
/// read when the metrics are collected: the program registers a callback, and the
/// callback reports the values that are true at that moment. An exponential histogram
/// is a histogram whose buckets adapt to the measurements rather than being fixed.
///
/// The OTLP exporter has its own kinds because a reader sees a sum, a gauge, a
/// histogram or a summary; this one says what the producer is allowed to do. A
/// counter and an up-down counter are both sums on the wire, but only one of them
/// may go down.
#derive(Eq, Debug)
pub(all) enum InstrumentKind {
  Counter
  UpDownCounter
  Histogram
  Gauge
  ObservableCounter
  ObservableUpDownCounter
  ObservableGauge
  ExponentialHistogram
}

///|
pub extend InstrumentKind with Eq::{equal, not_equal}

///|
pub extend InstrumentKind with Debug::{to_repr}

///|
/// How much one instrument keeps.
///
/// The attribute limits are OpenTelemetry's own: 128 attributes on a point, and
/// no limit on the length of a string value. `series` is the SDK's default
/// cardinality limit of 2000 per instrument, and `exemplars` is how many sampled
/// traces are kept beside a series. All four exist so a loop over user data cannot
/// make one instrument unbounded.
#derive(Eq, Debug)
pub(all) struct MeterLimits {
  attributes : Int32
  attribute_value_length : Int32?
  series : Int32
  exemplars : Int32
}

///|
pub extend MeterLimits with Eq::{equal, not_equal}

///|
pub extend MeterLimits with Debug::{to_repr}

///|
/// The OpenTelemetry defaults, from the metrics SDK specification's attribute
/// limits and cardinality limit sections.
pub let meter_limits : MeterLimits = {
  attributes: 128,
  attribute_value_length: None,
  series: 2000,
  exemplars: 1,
}

///|
/// The same limits, with any of them changed.
pub fn MeterLimits::new(
  attributes~ : Int32 = meter_limits.attributes,
  attribute_value_length~ : Int32? = meter_limits.attribute_value_length,
  series~ : Int32 = meter_limits.series,
  exemplars~ : Int32 = meter_limits.exemplars,
) -> MeterLimits {
  { attributes, attribute_value_length, series, exemplars, }
}

///|
/// The longest instrument name the metrics specification allows.
pub let instrument_name_limit : Int32 = 255

///|
/// Why a name or a bucket list was refused.
#derive(Eq, Debug)
pub(all) suberror Refused {
  /// The empty string is not a name.
  EmptyName
  /// A name that does not start with an ASCII letter, or carries a character the
  /// specification does not allow. Dots, slashes, hyphens and underscores are
  /// allowed, which is what a semantic convention name needs.
  InvalidName(name~ : String)
  /// A name longer than `instrument_name_limit`.
  LongName(name~ : String, got~ : Int32, limit~ : Int32)
  /// A bucket boundary that is not finite, or is not greater than the one before
  /// it, at this position in the list.
  Bounds(name~ : String, at~ : Int32)
  /// A name already registered as a different kind of instrument.
  Conflict(name~ : String, got~ : InstrumentKind, want~ : InstrumentKind)
}

///|
pub extend Refused with Eq::{equal, not_equal}

///|
pub extend Refused with Debug::{to_repr}

///|
/// Whether `name` is an instrument name the specification allows.
pub fn is_valid_name(name : String) -> Bool {
  let length = name.length()
  if length == 0 || length > instrument_name_limit {
    return false
  }
  let mut first = true
  for c in name {
    let code = c.to_int()
    let letter = (code >= 0x41 && code <= 0x5a) ||
      (code >= 0x61 && code <= 0x7a)
    let digit = code >= 0x30 && code <= 0x39
    let separator = code == 0x2e || code == 0x2d || code == 0x5f || code == 0x2f
    if first {
      if !letter {
        return false
      }
      first = false
    } else if !(letter || digit || separator) {
      return false
    }
  }
  true
}

///|
fn validate_name(name : String) -> Unit raise Refused {
  if name.length() == 0 {
    raise Refused::EmptyName
  }
  if name.length() > instrument_name_limit {
    raise Refused::LongName(
      name~,
      got=name.length(),
      limit=instrument_name_limit,
    )
  }
  if !is_valid_name(name) {
    raise Refused::InvalidName(name~)
  }
}

///|
/// Bucket boundaries have to be finite and strictly increasing: a NaN or a
/// repeated boundary makes the bucket a measurement belongs to ambiguous.
fn validate_bounds(bounds : Array[Double], name : String) -> Unit raise Refused {
  if bounds.length() == 0 {
    raise Refused::Bounds(name~, at=0)
  }
  for i in 0.. 0 && !(bounds[i] > bounds[i - 1])) {
      raise Refused::Bounds(name~, at=i)
    }
  }
}

///|
fn finite(value : Double) -> Bool {
  value >= -1.7976931348623157e308 && value <= 1.7976931348623157e308
}

///|
/// Which measurements are kept as exemplars.
///
/// An exemplar is a sample of what a number was made of: the trace id and span id of
/// one request that landed in that bucket, so a reader can jump from "the ninth
/// bucket doubled" to the request behind it.
///
/// The default is `TraceBased`, which is the metrics SDK's default and the only
/// filter that keeps the promise the jump makes: an exemplar from a trace that was
/// not sampled names a span no backend holds, and one with no context at all names
/// nothing.
#derive(Eq, Debug)
pub(all) enum ExemplarFilter {
  /// Keep every measurement, with or without a trace context.
  AlwaysOn
  /// Keep none.
  AlwaysOff
  /// Keep a measurement only when it arrived inside a sampled trace.
  TraceBased
}

///|
pub extend ExemplarFilter with Eq::{equal, not_equal}

///|
pub extend ExemplarFilter with Debug::{to_repr}