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

///|
/// A parsed sample line before it is folded into a family.
priv struct RawSample {
  name : String
  labels : Array[(String, String)]
  value : Double
  timestamp : Double?
  ex_labels : Array[(String, String)]
  ex_value : Double?
  ex_timestamp : Double?
}

///|
priv struct Cursor {
  chars : Array[Char]
  mut i : Int32
}

///|
fn Cursor::new(s : String) -> Cursor {
  { chars: s.to_array(), i: 0, }
}

///|
fn Cursor::at_end(self : Cursor) -> Bool {
  self.i >= self.chars.length()
}

///|
fn Cursor::peek(self : Cursor) -> Char? {
  if self.i < self.chars.length() {
    Some(self.chars[self.i])
  } else {
    None
  }
}

///|
fn Cursor::bump(self : Cursor) -> Unit {
  self.i = self.i + 1
}

///|
fn Cursor::skip_space(self : Cursor) -> Unit {
  while !self.at_end() {
    let c = self.chars[self.i]
    if c == ' ' || c == '\t' {
      self.bump()
    } else {
      break
    }
  }
}

///|
fn Cursor::take_until(self : Cursor, stop : (Char) -> Bool) -> String {
  let sb = StringBuilder()
  while !self.at_end() && !stop(self.chars[self.i]) {
    sb.write_char(self.chars[self.i])
    self.bump()
  }
  sb.to_string()
}

///|
fn parse_float(s : String) -> Double? {
  Some(@string.parse_double(s.trim())) catch {
    _ => None
  }
}

///|
fn parse_label_value(c : Cursor) -> String raise @metric.Refused {
  match c.peek() {
    Some('"') => c.bump()
    _ =>
      raise @metric.Refused(
        "expected a quoted label value at " + c.i.to_string(),
      )
  }
  let sb = StringBuilder()
  while !c.at_end() {
    let ch = c.chars[c.i]
    if ch == '"' {
      c.bump()
      return sb.to_string()
    }
    if ch == '\\' {
      c.bump()
      if c.at_end() {
        raise @metric.Refused("unterminated escape in label value")
      }
      let e = c.chars[c.i]
      if e == '\\' {
        sb.write_char('\\')
      } else if e == '"' {
        sb.write_char('"')
      } else if e == 'n' {
        sb.write_char('\n')
      } else {
        sb.write_char(e)
      }
      c.bump()
    } else {
      sb.write_char(ch)
      c.bump()
    }
  }
  raise @metric.Refused("unterminated label value")
}

///|
fn parse_labels(c : Cursor) -> Array[(String, String)] raise @metric.Refused {
  let out : Array[(String, String)] = []
  c.skip_space()
  match c.peek() {
    Some('{') => c.bump()
    _ => return out
  }
  c.skip_space()
  match c.peek() {
    Some('}') => {
      c.bump()
      return out
    }
    _ => ()
  }
  while true {
    c.skip_space()
    let name = c.take_until(fn(ch) {
      ch == '=' || ch == ' ' || ch == '}' || ch == ','
    })
    if name == "" {
      raise @metric.Refused("empty label name at " + c.i.to_string())
    }
    c.skip_space()
    match c.peek() {
      Some('=') => c.bump()
      _ => raise @metric.Refused("expected = after label name " + name)
    }
    c.skip_space()
    let value = parse_label_value(c)
    out.push((name, value))
    c.skip_space()
    match c.peek() {
      Some(',') => {
        c.bump()
        continue
      }
      Some('}') => {
        c.bump()
        break
      }
      _ => raise @metric.Refused("expected , or } in label set")
    }
  }
  out
}

///|
fn parse_sample(line : String) -> RawSample raise @metric.Refused {
  let c = Cursor::new(line)
  c.skip_space()
  let name = c.take_until(fn(ch) { ch == '{' || ch == ' ' || ch == '\t' })
  if name == "" {
    raise @metric.Refused("empty sample name")
  }
  let labels = parse_labels(c)
  c.skip_space()
  let value_token = c.take_until(fn(ch) { ch == ' ' || ch == '\t' })
  let value = match parse_float(value_token) {
    Some(v) => v
    None => raise @metric.Refused("bad sample value: " + value_token)
  }
  c.skip_space()
  let mut timestamp : Double? = None
  let mut ex_labels : Array[(String, String)] = []
  let mut ex_value : Double? = None
  let mut ex_timestamp : Double? = None
  match c.peek() {
    Some('#') => {
      c.bump()
      c.skip_space()
      ex_labels = parse_labels(c)
      c.skip_space()
      let t = c.take_until(fn(ch) { ch == ' ' || ch == '\t' })
      ex_value = parse_float(t)
      c.skip_space()
      if !c.at_end() {
        let ts = c.take_until(fn(ch) { ch == ' ' || ch == '\t' })
        ex_timestamp = parse_float(ts)
      }
    }
    Some(_) => {
      let t = c.take_until(fn(ch) { ch == ' ' || ch == '\t' })
      timestamp = parse_float(t)
      c.skip_space()
      match c.peek() {
        Some('#') => {
          c.bump()
          c.skip_space()
          ex_labels = parse_labels(c)
          c.skip_space()
          let v = c.take_until(fn(ch) { ch == ' ' || ch == '\t' })
          ex_value = parse_float(v)
          c.skip_space()
          if !c.at_end() {
            let ts = c.take_until(fn(ch) { ch == ' ' || ch == '\t' })
            ex_timestamp = parse_float(ts)
          }
        }
        _ => ()
      }
    }
    None => ()
  }
  { name, labels, value, timestamp, ex_labels, ex_value, ex_timestamp, }
}

///|
priv struct Staged {
  name : String
  mut kind : @metric.Kind?
  help : String
  mut unit : String?
  samples : Array[RawSample]
}

///|
fn strip_prefix(s : String, prefix : String) -> String? {
  if s.length() >= prefix.length() && s[0:prefix.length()].to_owned() == prefix {
    Some(s[prefix.length():].to_owned())
  } else {
    None
  }
}

///|
fn strip_suffix(s : String, suffix : String) -> String? {
  if s.length() >= suffix.length() &&
    s[s.length() - suffix.length():].to_owned() == suffix {
    Some(s[0:s.length() - suffix.length()].to_owned())
  } else {
    None
  }
}

///|
fn unescape_help(s : String) -> String {
  let sb = StringBuilder()
  let chars = s.to_array()
  let mut i = 0
  while i < chars.length() {
    if chars[i] == '\\' && i + 1 < chars.length() {
      let n = chars[i + 1]
      if n == '\\' {
        sb.write_char('\\')
        i = i + 2
      } else if n == 'n' {
        sb.write_char('\n')
        i = i + 2
      } else if n == '"' {
        sb.write_char('"')
        i = i + 2
      } else {
        sb.write_char(chars[i])
        i = i + 1
      }
    } else {
      sb.write_char(chars[i])
      i = i + 1
    }
  }
  sb.to_string()
}

///|
fn label_key(labels : Array[(String, String)]) -> String {
  let sorted : Array[(String, String)] = []
  for p in labels {
    sorted.push(p)
  }
  sorted.sort_by(fn(a, b) { String::lexical_compare(a.0, b.0) })
  let sb = StringBuilder()
  for p in sorted {
    sb.write_string(p.0.length().to_string())
    sb.write_char(':')
    sb.write_string(p.0)
    sb.write_string(p.1.length().to_string())
    sb.write_char(':')
    sb.write_string(p.1)
  }
  sb.to_string()
}

///|
priv struct Group {
  labels : Array[(String, String)]
  mut number : Double?
  mut sum : Double?
  mut count : Double?
  mut created : Double?
  buckets : Array[(Double, Double)]
  quantiles : Array[(Double, Double)]
  states : Array[(String, Bool)]
  exemplars : Array[@metric.Exemplar]
}

///|
fn group_for(
  groups : Map[String, Group],
  labels : Array[(String, String)],
) -> Group {
  let key = label_key(labels)
  match groups.get(key) {
    Some(g) => g
    None => {
      let g : Group = {
        labels,
        number: None,
        sum: None,
        count: None,
        created: None,
        buckets: [],
        quantiles: [],
        states: [],
        exemplars: [],
      }
      groups[key] = g
      g
    }
  }
}

///|
fn sort_buckets(buckets : Array[(Double, Double)]) -> Array[(Double, Double)] {
  let out : Array[(Double, Double)] = []
  for b in buckets {
    out.push(b)
  }
  out.sort_by(fn(a, b) {
    if a.0 < b.0 {
      -1
    } else if a.0 > b.0 {
      1
    } else {
      0
    }
  })
  out
}

///|
fn finish_counter(staged : Staged) -> @metric.MetricFamily {
  let kind = @metric.Kind::Counter
  let base = match strip_suffix(staged.name, "_total") {
    Some(b) => b
    None => staged.name
  }
  let groups : Map[String, Group] = Map([])
  for s in staged.samples {
    if s.name == base + "_total" {
      let g = group_for(groups, s.labels)
      g.number = Some(s.value)
      match s.ex_value {
        Some(v) =>
          g.exemplars.push({
            labels: s.ex_labels,
            value: v,
            timestamp: s.ex_timestamp,
          })
        None => ()
      }
    } else if s.name == base + "_created" {
      let g = group_for(groups, s.labels)
      g.created = Some(s.value)
    }
  }
  let points : Array[@metric.Point] = []
  for _, g in groups {
    points.push({
      labels: g.labels,
      value: @metric.Value::Number(g.number.unwrap_or(0.0)),
      timestamp: None,
      created: g.created,
      exemplars: g.exemplars,
    })
  }
  { name: base, kind, help: staged.help, unit: staged.unit, metrics: points, }
}

///|
fn finish_histogram(
  staged : Staged,
  kind : @metric.Kind,
) -> @metric.MetricFamily {
  let base = staged.name
  let (count_name, sum_name) = match kind {
    @metric.Kind::GaugeHistogram => ("_gcount", "_gsum")
    _ => ("_count", "_sum")
  }
  let groups : Map[String, Group] = Map([])
  for s in staged.samples {
    if s.name == base + "_bucket" {
      let mut le = 0.0
      let mut has_le = false
      let rest : Array[(String, String)] = []
      for p in s.labels {
        if p.0 == "le" {
          has_le = true
          match parse_float(p.1) {
            Some(v) => le = v
            None => ()
          }
        } else {
          rest.push(p)
        }
      }
      if has_le {
        let g = group_for(groups, rest)
        g.buckets.push((le, s.value))
        match s.ex_value {
          Some(v) =>
            g.exemplars.push({
              labels: s.ex_labels,
              value: v,
              timestamp: s.ex_timestamp,
            })
          None => ()
        }
      }
    } else if s.name == base + count_name {
      let g = group_for(groups, s.labels)
      g.count = Some(s.value)
    } else if s.name == base + sum_name {
      let g = group_for(groups, s.labels)
      g.sum = Some(s.value)
    } else if s.name == base + "_created" {
      let g = group_for(groups, s.labels)
      g.created = Some(s.value)
    }
  }
  let points : Array[@metric.Point] = []
  for _, g in groups {
    let sorted = sort_buckets(g.buckets)
    let finite : Array[(Double, Int64)] = []
    let mut last = 0.0
    for b in sorted {
      if b.0 != 1.0 / 0.0 {
        finite.push((b.0, b.1.to_int64()))
      }
      last = b.1
    }
    let total = match g.count {
      Some(c) => c.to_int64()
      None => last.to_int64()
    }
    points.push({
      labels: g.labels,
      value: @metric.Value::Buckets(
        buckets=finite,
        sum=g.sum.unwrap_or(0.0),
        count=total,
      ),
      timestamp: None,
      created: g.created,
      exemplars: g.exemplars,
    })
  }
  { name: base, kind, help: staged.help, unit: staged.unit, metrics: points, }
}

///|
fn finish_summary(staged : Staged) -> @metric.MetricFamily {
  let base = staged.name
  let groups : Map[String, Group] = Map([])
  for s in staged.samples {
    let mut has_quantile = false
    for p in s.labels {
      if p.0 == "quantile" {
        has_quantile = true
      }
    }
    if s.name == base && has_quantile {
      let mut q = 0.0
      let rest : Array[(String, String)] = []
      for p in s.labels {
        if p.0 == "quantile" {
          match parse_float(p.1) {
            Some(v) => q = v
            None => ()
          }
        } else {
          rest.push(p)
        }
      }
      let g = group_for(groups, rest)
      g.quantiles.push((q, s.value))
    } else if s.name == base + "_count" {
      let g = group_for(groups, s.labels)
      g.count = Some(s.value)
    } else if s.name == base + "_sum" {
      let g = group_for(groups, s.labels)
      g.sum = Some(s.value)
    } else if s.name == base + "_created" {
      let g = group_for(groups, s.labels)
      g.created = Some(s.value)
    }
  }
  let points : Array[@metric.Point] = []
  for _, g in groups {
    let qs : Array[(Double, Double)] = []
    for q in g.quantiles {
      qs.push(q)
    }
    qs.sort_by(fn(a, b) {
      if a.0 < b.0 {
        -1
      } else if a.0 > b.0 {
        1
      } else {
        0
      }
    })
    points.push({
      labels: g.labels,
      value: @metric.Value::Quantiles(
        quantiles=qs,
        sum=g.sum.unwrap_or(0.0),
        count=g.count.unwrap_or(0.0).to_int64(),
      ),
      timestamp: None,
      created: g.created,
      exemplars: [],
    })
  }
  {
    name: base,
    kind: @metric.Kind::Summary,
    help: staged.help,
    unit: staged.unit,
    metrics: points,
  }
}

///|
fn finish_info(staged : Staged) -> @metric.MetricFamily {
  let mut om_style = false
  for s in staged.samples {
    if s.name == staged.name + "_info" {
      om_style = true
    }
  }
  let base = if om_style {
    staged.name
  } else {
    match strip_suffix(staged.name, "_info") {
      Some(b) => b
      None => staged.name
    }
  }
  let points : Array[@metric.Point] = []
  for s in staged.samples {
    points.push({
      labels: s.labels,
      value: @metric.Value::Number(1.0),
      timestamp: s.timestamp,
      created: None,
      exemplars: [],
    })
  }
  {
    name: base,
    kind: @metric.Kind::Info,
    help: staged.help,
    unit: staged.unit,
    metrics: points,
  }
}

///|
fn finish_stateset(staged : Staged) -> @metric.MetricFamily {
  let base = staged.name
  let groups : Map[String, Group] = Map([])
  for s in staged.samples {
    let mut state = ""
    let rest : Array[(String, String)] = []
    for p in s.labels {
      if p.0 == base {
        state = p.1
      } else {
        rest.push(p)
      }
    }
    let g = group_for(groups, rest)
    g.states.push((state, s.value != 0.0))
  }
  let points : Array[@metric.Point] = []
  for _, g in groups {
    points.push({
      labels: g.labels,
      value: @metric.Value::State(states=g.states),
      timestamp: None,
      created: None,
      exemplars: [],
    })
  }
  {
    name: base,
    kind: @metric.Kind::StateSet,
    help: staged.help,
    unit: staged.unit,
    metrics: points,
  }
}

///|
fn finish_plain(staged : Staged, kind : @metric.Kind) -> @metric.MetricFamily {
  let points : Array[@metric.Point] = []
  for s in staged.samples {
    if s.name == staged.name {
      points.push({
        labels: s.labels,
        value: @metric.Value::Number(s.value),
        timestamp: s.timestamp,
        created: None,
        exemplars: [],
      })
    }
  }
  {
    name: staged.name,
    kind,
    help: staged.help,
    unit: staged.unit,
    metrics: points,
  }
}

///|
fn finish(staged : Staged) -> @metric.MetricFamily {
  match staged.kind {
    Some(@metric.Kind::Counter) => finish_counter(staged)
    Some(@metric.Kind::Histogram) =>
      finish_histogram(staged, @metric.Kind::Histogram)
    Some(@metric.Kind::GaugeHistogram) =>
      finish_histogram(staged, @metric.Kind::GaugeHistogram)
    Some(@metric.Kind::Summary) => finish_summary(staged)
    Some(@metric.Kind::Info) => finish_info(staged)
    Some(@metric.Kind::StateSet) => finish_stateset(staged)
    Some(k) => finish_plain(staged, k)
    None => finish_plain(staged, @metric.Kind::Unknown)
  }
}

///|
/// Parse an exposition back into families. The compatibility format is lossy for
/// info and state sets, which it spells as gauges; OpenMetrics keeps them. The
/// format argument selects which spelling conventions to expect, and the same
/// parser accepts both because the syntax is a superset.
pub fn parse(
  text : String,
  format~ : Format = Format::Text004,
) -> Array[@metric.MetricFamily] raise @metric.Refused {
  let _ = format
  let out : Array[@metric.MetricFamily] = []
  let mut current : Staged? = None
  for raw_line in text.split("\n") {
    let line = raw_line.to_owned()
    let line = if line.length() > 0 && line[line.length() - 1] == '\r' {
      line[0:line.length() - 1].to_owned()
    } else {
      line
    }
    if line == "" {
      continue
    }
    match strip_prefix(line, "# HELP ") {
      Some(rest) => {
        let c = Cursor::new(rest)
        let name = c.take_until(fn(ch) { ch == ' ' || ch == '\t' })
        c.skip_space()
        let help = c.take_until(fn(_ch) { false })
        match current {
          Some(staged) => out.push(finish(staged))
          None => ()
        }
        current = Some({
          name,
          kind: None,
          help: unescape_help(help),
          unit: None,
          samples: [],
        })
        continue
      }
      None => ()
    }
    match strip_prefix(line, "# TYPE ") {
      Some(rest) => {
        let c = Cursor::new(rest)
        let name = c.take_until(fn(ch) { ch == ' ' || ch == '\t' })
        c.skip_space()
        let spelling = c.take_until(fn(ch) { ch == ' ' || ch == '\t' })
        let kind = @metric.Kind::of(spelling)
        match current {
          Some(staged) =>
            if staged.name == name {
              staged.kind = Some(kind)
            } else {
              out.push(finish(staged))
              current = Some({
                name,
                kind: Some(kind),
                help: "",
                unit: None,
                samples: [],
              })
            }
          None =>
            current = Some({
              name,
              kind: Some(kind),
              help: "",
              unit: None,
              samples: [],
            })
        }
        continue
      }
      None => ()
    }
    match strip_prefix(line, "# UNIT ") {
      Some(rest) => {
        let c = Cursor::new(rest)
        let name = c.take_until(fn(ch) { ch == ' ' || ch == '\t' })
        c.skip_space()
        let unit = c.take_until(fn(_ch) { false })
        match current {
          Some(staged) => if staged.name == name { staged.unit = Some(unit) }
          None => ()
        }
        continue
      }
      None => ()
    }
    if line == "# EOF" {
      break
    }
    if strip_prefix(line, "#") is Some(_) {
      continue
    }
    let sample = parse_sample(line)
    match current {
      Some(staged) => staged.samples.push(sample)
      None =>
        current = Some({
          name: sample.name,
          kind: None,
          help: "",
          unit: None,
          samples: [sample],
        })
    }
  }
  match current {
    Some(staged) => out.push(finish(staged))
    None => ()
  }
  out
}