///|
/// Errors raised before a label vector creates or looks up a child metric.
pub(all) suberror VectorError {
  InvalidLabelName(String)
  ReservedLabelName(String)
  DuplicateLabelName(String)
  LabelCountMismatch(expected~ : Int, actual~ : Int)
  InvalidCardinalityLimit(Int)
  MissingLabel(String)
  UnexpectedLabel(String)
  CardinalityLimitExceeded(limit~ : Int, current~ : Int)
  MetricKindMismatch
  MetricConstructionFailed(String)
} derive(Debug, Eq)

///|
/// Validated variable-label layout shared by CounterVec/GaugeVec/HistogramVec.
///
/// The concrete vector types are P0 implementation work; this type fixes their
/// collision-free child-key and cardinality contracts at the package boundary.
pub struct LabelSchema {
  names : Array[String]
  cardinality_limit : Int?
}

///|
pub fn LabelSchema::new(
  names : Array[String],
  cardinality_limit? : Int,
) -> LabelSchema raise VectorError {
  for index, name in names {
    validate_schema_label_name(name)
    for previous in 0.. raise InvalidCardinalityLimit(limit)
    _ => ()
  }
  { names: names.copy(), cardinality_limit }
}

///|
pub fn LabelSchema::names(self : LabelSchema) -> Array[String] {
  self.names.copy()
}

///|
pub fn LabelSchema::cardinality_limit(self : LabelSchema) -> Int? {
  self.cardinality_limit
}

///|
pub fn LabelSchema::encode_key(
  self : LabelSchema,
  values : Array[String],
) -> String raise VectorError {
  if values.length() != self.names.length() {
    raise LabelCountMismatch(
      expected=self.names.length(),
      actual=values.length(),
    )
  }
  encode_label_values(values)
}

///|
/// Convert an unordered label map into this schema's positional value array.
///
/// A map must contain exactly the declared names. This prevents a missing
/// label from silently becoming the empty string and prevents typo labels from
/// creating an unbounded accidental dimension.
pub fn LabelSchema::values_from_labels(
  self : LabelSchema,
  labels : Map[String, String],
) -> Array[String] raise VectorError {
  for name in self.names {
    if !labels.contains(name) {
      raise MissingLabel(name)
    }
  }
  for name, _ in labels {
    if !schema_contains_name(self.names, name) {
      raise UnexpectedLabel(name)
    }
  }
  let values = []
  for name in self.names {
    values.push(labels[name])
  }
  values
}

///|
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 validate_schema_label_name(name : String) -> Unit raise VectorError {
  if name.has_prefix("__") {
    raise ReservedLabelName(name)
  }
  if name.is_empty() {
    raise InvalidLabelName(name)
  }
  let mut index = 0
  for char in name {
    let valid = if index == 0 {
      is_ascii_letter(char) || char == '_'
    } else {
      is_ascii_letter(char) || is_ascii_digit(char) || char == '_'
    }
    if !valid {
      raise InvalidLabelName(name)
    }
    index += 1
  }
}

///|
fn schema_contains_name(names : Array[String], name : String) -> Bool {
  for current in names {
    if current == name {
      return true
    }
  }
  false
}