///|
/// Metric kinds defined by OpenMetrics 1.0.
///
/// `Unknown` is useful for untyped input where no `# TYPE` directive was
/// present. `GaugeHistogram` is kept distinct from a regular histogram because
/// its `_gsum`/`_gcount` suffixes have different validation rules.
pub(all) enum MetricType {
  Unknown
  Counter
  Gauge
  Histogram
  GaugeHistogram
  Summary
  Info
  StateSet
} derive(Eq)

///|
/// Convert a metric type into its wire-format keyword.
pub fn MetricType::to_keyword(self : MetricType) -> String {
  match self {
    Unknown => "unknown"
    Counter => "counter"
    Gauge => "gauge"
    Histogram => "histogram"
    GaugeHistogram => "gaugehistogram"
    Summary => "summary"
    Info => "info"
    StateSet => "stateset"
  }
}

///|
/// Parse a `# TYPE` keyword.
pub fn MetricType::from_keyword(
  keyword : StringView,
) -> Result[MetricType, String] {
  match keyword {
    "unknown" => Ok(Unknown)
    "counter" => Ok(Counter)
    "gauge" => Ok(Gauge)
    "histogram" => Ok(Histogram)
    "gaugehistogram" => Ok(GaugeHistogram)
    "summary" => Ok(Summary)
    "info" => Ok(Info)
    "stateset" => Ok(StateSet)
    other => Err("unsupported metric type '\{other}'")
  }
}

///|
/// A label name/value pair attached to a sample or exemplar.
pub(all) struct Label {
  name : String
  value : String
} derive(Eq)

///|
/// Create a label.
pub fn Label::new(name : String, value : String) -> Label {
  { name, value }
}

///|
/// An OpenMetrics exemplar attached to a sample.
///
/// An exemplar contains its own label set, a numeric observation and an
/// optional Unix timestamp in seconds.
pub(all) struct Exemplar {
  labels : Array[Label]
  value : Double
  timestamp : Double?
} derive(Eq)

///|
/// Create an exemplar without a timestamp.
pub fn Exemplar::new(
  labels : Array[Label],
  value : Double,
  timestamp? : Double,
) -> Exemplar {
  { labels, value, timestamp }
}

///|
/// One sample line in an exposition document.
pub(all) struct Sample {
  name : String
  labels : Array[Label]
  value : Double
  timestamp : Double?
  exemplar : Exemplar?
} derive(Eq)

///|
/// Create a sample.
pub fn Sample::new(
  name : String,
  value : Double,
  labels? : Array[Label] = [],
  timestamp? : Double,
  exemplar? : Exemplar,
) -> Sample {
  { name, labels, value, timestamp, exemplar }
}

///|
/// Return the first value for a label name.
pub fn Sample::label(self : Sample, name : StringView) -> String? {
  for label in self.labels {
    if name.equal_to_string(label.name) {
      return Some(label.value)
    }
  }
  None
}

///|
/// Metadata and samples belonging to one metric family.
pub(all) struct MetricFamily {
  name : String
  help : String?
  metric_type : MetricType
  unit : String?
  samples : Array[Sample]
} derive(Eq)

///|
/// Create an empty metric family.
pub fn MetricFamily::new(
  name : String,
  metric_type? : MetricType = Unknown,
  help? : String,
  unit? : String,
) -> MetricFamily {
  let help = match help {
    Some(value) => if value == "" { None } else { Some(value) }
    None => None
  }
  let unit = match unit {
    Some(value) => if value == "" { None } else { Some(value) }
    None => None
  }
  { name, help, metric_type, unit, samples: [] }
}

///|
/// Return a copy of a family with one extra sample.
pub fn MetricFamily::add_sample(
  self : MetricFamily,
  sample : Sample,
) -> MetricFamily {
  let samples = self.samples.copy()
  samples.push(sample)
  { ..self, samples, }
}

///|
/// A parsed OpenMetrics document.
///
/// `has_eof` records whether the mandatory `# EOF` marker was present. The
/// parser preserves a document without the marker so callers can choose
/// between lenient inspection and strict validation.
pub(all) struct Document {
  families : Array[MetricFamily]
  has_eof : Bool
} derive(Eq)

///|
/// Create an empty document.
pub fn Document::new() -> Document {
  { families: [], has_eof: false }
}

///|
/// Find a family by its declared name.
pub fn Document::family(self : Document, name : StringView) -> MetricFamily? {
  for family in self.families {
    if name.equal_to_string(family.name) {
      return Some(family)
    }
  }
  None
}

///|
/// Severity of a semantic validation issue.
pub(all) enum Severity {
  Warning
  Error
} derive(Eq)

///|
/// A machine-readable semantic validation issue.
pub(all) struct ValidationIssue {
  severity : Severity
  code : String
  family : String
  sample : String?
  message : String
} derive(Eq)

///|
/// Create a validation error.
pub fn ValidationIssue::error(
  code : String,
  family : String,
  message : String,
  sample? : String,
) -> ValidationIssue {
  { severity: Error, code, family, sample, message }
}

///|
/// Create a validation warning.
pub fn ValidationIssue::warning(
  code : String,
  family : String,
  message : String,
  sample? : String,
) -> ValidationIssue {
  { severity: Warning, code, family, sample, message }
}

///|
/// Syntax failure returned by the line-oriented parser.
pub(all) struct ParseError {
  line : Int
  column : Int
  message : String
} derive(Eq)

///|
/// Create a parser error at a one-based line and column.
pub fn ParseError::new(
  line : Int,
  column : Int,
  message : String,
) -> ParseError {
  { line, column, message }
}

///|
/// Render a compact human-readable parser error.
pub fn ParseError::to_string(self : ParseError) -> String {
  "line \{self.line}, column \{self.column}: \{self.message}"
}