///|
fn label_value(labels : @model.LabelSet, name : String) -> String {
  labels.get(name).unwrap_or("")
}

///|
fn regex_matches(
  pattern : String,
  value : String,
) -> Bool raise @model.PromqlError {
  let anchored = "^(?:\{pattern})$"
  let regex = @string.Regex(anchored) catch {
    _ => raise @model.EvalError("invalid regular expression `\{pattern}`")
  }
  regex.execute(value) is Some(_)
}

///|
fn matcher_matches(
  labels : @model.LabelSet,
  matcher : @ast.LabelMatcher,
) -> Bool raise @model.PromqlError {
  let actual = label_value(labels, matcher.name)
  match matcher.op {
    @ast.MatchOp::Equal => actual == matcher.value
    @ast.MatchOp::NotEqual => actual != matcher.value
    @ast.MatchOp::Regex => regex_matches(matcher.value, actual)
    @ast.MatchOp::NotRegex => !regex_matches(matcher.value, actual)
  }
}

///|
fn selector_matches(
  series : @model.Series,
  selector : @ast.Selector,
) -> Bool raise @model.PromqlError {
  if selector.metric != "" && series.metric != selector.metric {
    return false
  }
  for matcher in selector.matchers {
    if !matcher_matches(series.labels, matcher) {
      return false
    }
  }
  true
}

///|
fn selector_eval_time(
  selector : @ast.Selector,
  context : EvalContext,
) -> Double {
  let base = selector.at_ms.unwrap_or(context.eval_time_ms)
  base - selector.offset_ms
}

///|
fn latest_sample(
  samples : Array[@model.Sample],
  end_ms : Double,
) -> @model.Sample? {
  let mut result : @model.Sample? = None
  for sample in samples {
    if sample.timestamp <= end_ms {
      if result is None || sample.timestamp >= result.unwrap().timestamp {
        result = Some(sample)
      }
    } else {
      break
    }
  }
  match result {
    None => None
    Some(sample) if end_ms - sample.timestamp > 300000.0 => None
    Some(_) => result
  }
}

///|
fn select_instant(
  selector : @ast.Selector,
  context : EvalContext,
) -> @model.Value raise @model.PromqlError {
  let end_ms = selector_eval_time(selector, context)
  let points : Array[@model.Point] = []
  for series in context.series {
    if selector_matches(series, selector) {
      match latest_sample(series.samples, end_ms) {
        Some(sample) =>
          points.push({ labels: series.labels, value: sample.value, })
        None => ()
      }
    }
  }
  points.sort_by((left, right) => {
    left.labels.to_string().compare(right.labels.to_string())
  })
  @model.Instant(points)
}

///|
fn select_range(
  selector : @ast.Selector,
  range_ms : Double,
  context : EvalContext,
) -> @model.Value raise @model.PromqlError {
  let end_ms = selector_eval_time(selector, context)
  let start_ms = end_ms - range_ms
  let output : Array[@model.RangeSeries] = []
  for series in context.series {
    if selector_matches(series, selector) {
      let samples : Array[@model.Sample] = []
      for sample in series.samples {
        if sample.timestamp > start_ms && sample.timestamp <= end_ms {
          samples.push(sample)
        }
      }
      if !samples.is_empty() {
        output.push({ labels: series.labels, samples, })
      }
    }
  }
  output.sort_by((left, right) => {
    left.labels.to_string().compare(right.labels.to_string())
  })
  @model.Range(output)
}