///|
priv struct Group {
  labels : @model.LabelSet
  points : Array[@model.Point]
}

///|
fn grouping_labels(
  labels : @model.LabelSet,
  without : Bool,
  names : Array[String],
) -> @model.LabelSet {
  if without {
    let excluded : Array[String] = ["__name__"]
    for name in names {
      excluded.push(name)
    }
    labels.without(excluded)
  } else {
    labels.only(names)
  }
}

///|
fn aggregate_value(
  operator : @ast.AggOp,
  group : Group,
) -> Double raise @model.PromqlError {
  if group.points.is_empty() {
    raise @model.EvalError("aggregation received an empty group")
  }
  match operator {
    @ast.AggOp::Sum => {
      let mut result = 0.0
      for point in group.points {
        result += point.value
      }
      result
    }
    @ast.AggOp::Avg => {
      let mut result = 0.0
      for point in group.points {
        result += point.value
      }
      result / group.points.length().to_double()
    }
    @ast.AggOp::Min => {
      let mut result = group.points[0].value
      for point in group.points {
        result = result.min(point.value)
      }
      result
    }
    @ast.AggOp::Max => {
      let mut result = group.points[0].value
      for point in group.points {
        result = result.max(point.value)
      }
      result
    }
    @ast.AggOp::Count => group.points.length().to_double()
    _ => raise @model.EvalError("topk and bottomk need a scalar parameter")
  }
}

///|
fn evaluate_aggregate(
  operator : @ast.AggOp,
  args : Array[@ast.Expr],
  without : Bool,
  names : Array[String],
  context : EvalContext,
) -> @model.Value raise @model.PromqlError {
  if operator is @ast.AggOp::TopK || operator is @ast.AggOp::BottomK {
    if args.length() != 2 {
      raise @model.EvalError("topk and bottomk expect a scalar and a vector")
    }
    let count_value = evaluate(args[0], context)
    let count = match count_value {
      @model.Scalar(value) => value
      _ => raise @model.EvalError("topk and bottomk count must be a scalar")
    }
    let points = match evaluate(args[1], context) {
      @model.Instant(points) => points
      value =>
        raise @model.EvalError(
          "topk and bottomk expect an instant vector, got \{value.kind()}",
        )
    }
    let groups : Array[Group] = []
    for point in points {
      let labels = grouping_labels(point.labels, without, names)
      let mut found = false
      for group in groups {
        if group.labels == labels {
          group.points.push(point)
          found = true
          break
        }
      }
      if !found {
        groups.push({ labels, points: [point], })
      }
    }
    let output : Array[@model.Point] = []
    for group in groups {
      group.points.sort_by((left, right) => {
        let order = if operator is @ast.AggOp::TopK {
          right.value.compare(left.value)
        } else {
          left.value.compare(right.value)
        }
        if order == 0 {
          left.labels.to_string().compare(right.labels.to_string())
        } else {
          order
        }
      })
      let limit = if count <= 0.0 || count != count {
        0
      } else if count >= group.points.length().to_double() {
        group.points.length()
      } else {
        count.to_int()
      }
      for index in 0.. points
    value =>
      raise @model.EvalError(
        "aggregation expects an instant vector, got \{value.kind()}",
      )
  }
  let groups : Array[Group] = []
  for point in input {
    let labels = grouping_labels(point.labels, without, names)
    let mut found = false
    for group in groups {
      if group.labels == labels {
        group.points.push(point)
        found = true
        break
      }
    }
    if !found {
      groups.push({ labels, points: [point], })
    }
  }
  let output : Array[@model.Point] = []
  for group in groups {
    output.push({
      labels: group.labels,
      value: aggregate_value(operator, group),
    })
  }
  output.sort_by((left, right) => {
    left.labels.to_string().compare(right.labels.to_string())
  })
  @model.Instant(output)
}