///|
pub(all) struct MetricLabel {
  name : String
  value : String
} derive(Eq, Debug)

///|
pub fn metric_label(name : String, value : String) -> MetricLabel {
  { name, value }
}

///|
pub fn export_prometheus(
  snapshot : MetricsSnapshot,
  prefix? : String = "moonresilience",
  labels? : Array[MetricLabel] = [],
) -> Result[String, String] {
  if !valid_metric_name(prefix) {
    return Err("invalid metric prefix: " + prefix)
  }
  for label in labels {
    if !valid_metric_name(label.name) {
      return Err("invalid label name: " + label.name)
    }
    if label.value.contains("\n") || label.value.contains("\r") {
      return Err("label value must not contain a newline: " + label.name)
    }
  }
  let suffix = format_labels(labels)
  let lines : Array[String] = []
  append_metric(
    lines,
    prefix,
    "executions_total",
    "Total policy executions.",
    snapshot.executions,
    suffix,
  )
  append_metric(
    lines,
    prefix,
    "successes_total",
    "Total successful executions.",
    snapshot.successes,
    suffix,
  )
  append_metric(
    lines,
    prefix,
    "failures_total",
    "Total failed executions.",
    snapshot.failures,
    suffix,
  )
  append_metric(
    lines,
    prefix,
    "retries_total",
    "Total scheduled retries.",
    snapshot.retries,
    suffix,
  )
  append_metric(
    lines,
    prefix,
    "circuit_opens_total",
    "Total circuit-open transitions.",
    snapshot.circuit_opens,
    suffix,
  )
  append_metric(
    lines,
    prefix,
    "rate_limit_rejections_total",
    "Total rate limiter rejections.",
    snapshot.rate_limit_rejections,
    suffix,
  )
  append_metric(
    lines,
    prefix,
    "bulkhead_rejections_total",
    "Total bulkhead rejections.",
    snapshot.bulkhead_rejections,
    suffix,
  )
  append_gauge(
    lines,
    prefix,
    "latency_average_milliseconds",
    "Average virtual execution latency.",
    snapshot.average_latency_ms,
    suffix,
  )
  append_gauge(
    lines,
    prefix,
    "latency_max_milliseconds",
    "Maximum virtual execution latency.",
    snapshot.max_latency_ms,
    suffix,
  )
  Ok(join_lines(lines))
}

///|
pub fn export_latency_prometheus(
  snapshot : LatencySnapshot,
  prefix? : String = "moonresilience",
  labels? : Array[MetricLabel] = [],
) -> Result[String, String] {
  if !valid_metric_name(prefix) {
    return Err("invalid metric prefix: " + prefix)
  }
  let base_labels = format_labels(labels)
  let lines : Array[String] = []
  lines.push(
    "# HELP " + prefix + "_latency_bucket Observed execution latencies.",
  )
  lines.push("# TYPE " + prefix + "_latency_bucket counter")
  let mut cumulative = 0
  for bucket in snapshot.buckets {
    cumulative = cumulative + bucket.count
    let bucket_labels = append_label_text(
      base_labels,
      "le",
      bucket.upper_bound_ms.to_string(),
    )
    lines.push(
      prefix + "_latency_bucket" + bucket_labels + " " + cumulative.to_string(),
    )
  }
  let infinity_labels = append_label_text(base_labels, "le", "+Inf")
  lines.push(
    prefix +
    "_latency_bucket" +
    infinity_labels +
    " " +
    snapshot.count.to_string(),
  )
  append_gauge(
    lines,
    prefix,
    "latency_count",
    "Total observed latencies.",
    snapshot.count,
    base_labels,
  )
  append_gauge(
    lines,
    prefix,
    "latency_max_milliseconds",
    "Maximum observed latency.",
    snapshot.max_ms,
    base_labels,
  )
  Ok(join_lines(lines))
}

///|
fn append_metric(
  lines : Array[String],
  prefix : String,
  name : String,
  help : String,
  value : Int,
  labels : String,
) -> Unit {
  let full_name = prefix + "_" + name
  lines.push("# HELP " + full_name + " " + help)
  lines.push("# TYPE " + full_name + " counter")
  lines.push(full_name + labels + " " + value.to_string())
}

///|
fn append_gauge(
  lines : Array[String],
  prefix : String,
  name : String,
  help : String,
  value : Int,
  labels : String,
) -> Unit {
  let full_name = prefix + "_" + name
  lines.push("# HELP " + full_name + " " + help)
  lines.push("# TYPE " + full_name + " gauge")
  lines.push(full_name + labels + " " + value.to_string())
}

///|
fn format_labels(labels : Array[MetricLabel]) -> String {
  if labels.length() == 0 {
    return ""
  }
  let mut text = "{"
  for index = 0; index < labels.length(); index = index + 1 {
    if index > 0 {
      text = text + ","
    }
    let label = labels[index]
    text = text + label.name + "=\"" + escape_label_value(label.value) + "\""
  }
  text + "}"
}

///|
fn append_label_text(labels : String, name : String, value : String) -> String {
  if labels.length() == 0 {
    "{" + name + "=\"" + value + "\"}"
  } else {
    labels[:labels.length() - 1].to_owned() + "," + name + "=\"" + value + "\"}"
  }
}

///|
fn escape_label_value(value : String) -> String {
  let mut output = ""
  for char in value {
    match char {
      '\\' => output = output + "\\\\"
      '"' => output = output + "\\\""
      _ => output = output + char.to_string()
    }
  }
  output
}

///|
fn valid_metric_name(name : String) -> Bool {
  if name.length() == 0 {
    return false
  }
  for index = 0; index < name.length(); index = index + 1 {
    let char = name[index]
    let allowed = (char >= 'a' && char <= 'z') ||
      (char >= 'A' && char <= 'Z') ||
      char == '_' ||
      (index > 0 && char >= '0' && char <= '9')
    if !allowed {
      return false
    }
  }
  true
}

///|
fn join_lines(lines : Array[String]) -> String {
  let mut output = ""
  for index = 0; index < lines.length(); index = index + 1 {
    output = output + lines[index] + "\n"
  }
  output
}