///|
pub(all) struct ObservationSeries {
  name : String
  window_minutes : Int
  samples : Array[TrafficSample]
} derive(Eq, Debug)

///|
pub fn ObservationSeries::new(
  name : String,
  window_minutes : Int,
) -> ObservationSeries {
  {
    name,
    window_minutes: if window_minutes > 0 {
      window_minutes
    } else {
      1
    },
    samples: [],
  }
}

///|
pub fn ObservationSeries::from_samples(
  name : String,
  window_minutes : Int,
  samples : Array[TrafficSample],
) -> ObservationSeries {
  let series = ObservationSeries::new(name, window_minutes)
  for sample in samples {
    series.samples.push(sample)
  }
  series
}

///|
pub fn ObservationSeries::add(
  self : ObservationSeries,
  sample : TrafficSample,
) -> ObservationSeries {
  self.samples.push(sample)
  self
}

///|
pub fn ObservationSeries::sample_count(self : ObservationSeries) -> Int {
  self.samples.length()
}

///|
pub fn ObservationSeries::first_minute(self : ObservationSeries) -> Int {
  if self.samples.length() == 0 {
    0
  } else {
    self.samples[0].minute
  }
}

///|
pub fn ObservationSeries::last_minute(self : ObservationSeries) -> Int {
  let mut last = 0
  for sample in self.samples {
    if sample.minute > last {
      last = sample.minute
    }
  }
  last
}

///|
pub fn ObservationSeries::window_at(
  self : ObservationSeries,
  now_minute : Int,
) -> RequestWindow {
  aggregate_recent_samples(
    self.name,
    now_minute,
    self.window_minutes,
    self.samples,
  )
}

///|
pub fn ObservationSeries::window_from(
  self : ObservationSeries,
  start_minute : Int,
  end_minute : Int,
) -> RequestWindow {
  let mut total = 0
  let mut good = 0
  for sample in self.samples {
    if sample.minute >= start_minute && sample.minute <= end_minute {
      total = total + sample.total
      good = good + sample.good
    }
  }
  RequestWindow::new(
    self.name,
    if end_minute >= start_minute {
      end_minute - start_minute + 1
    } else {
      1
    },
    total,
    good,
  )
}

///|
pub fn ObservationSeries::windows(
  self : ObservationSeries,
) -> Array[RequestWindow] {
  let result = []
  for sample in self.samples {
    result.push(self.window_at(sample.minute))
  }
  result
}

///|
pub fn ObservationSeries::budget_reports(
  self : ObservationSeries,
  target : SloTarget,
) -> Array[BudgetReport] {
  let result = []
  for window in self.windows() {
    result.push(evaluate_budget(target, window))
  }
  result
}

///|
pub fn ObservationSeries::burn_reports(
  self : ObservationSeries,
  target : SloTarget,
) -> Array[BurnReport] {
  let result = []
  for window in self.windows() {
    result.push(calculate_burn_rate(target, window))
  }
  result
}

///|
pub fn ObservationSeries::peak_burn(
  self : ObservationSeries,
  target : SloTarget,
) -> Int {
  let mut peak = 0
  for report in self.burn_reports(target) {
    if report.burn_rate_x100 > peak {
      peak = report.burn_rate_x100
    }
  }
  peak
}

///|
pub fn ObservationSeries::to_json(self : ObservationSeries) -> String {
  let result = StringBuilder()
  result.write_string(
    "{\"name\":\"\{escape_json(self.name)}\",\"window_minutes\":\{self.window_minutes},\"samples\":[",
  )
  for i, sample in self.samples {
    if i > 0 {
      result.write_string(",")
    }
    result.write_string(
      "{\"minute\":\{sample.minute},\"total\":\{sample.total},\"good\":\{sample.good}}",
    )
  }
  result.write_string("]}")
  result.to_string()
}

///|
pub fn ObservationSeries::coverage_minutes(self : ObservationSeries) -> Int {
  if self.samples.length() == 0 {
    0
  } else {
    self.last_minute() - self.first_minute() + 1
  }
}

///|
pub fn ObservationSeries::is_continuous(self : ObservationSeries) -> Bool {
  if self.samples.length() <= 1 {
    true
  } else {
    for i in 1..