///|
#derive(Debug, Eq)
pub(all) suberror ArchiveError {
  InvalidConfig(String)
  InvalidSample(String)
  InvalidSnapshot(String)
}

///|
#derive(Debug, Eq)
pub(all) enum Consolidation {
  Average
  Minimum
  Maximum
  Last
}

///|
#derive(Debug, Eq)
pub struct Spec {
  priv factor : Int32
  priv rows : Int32
  priv aggregation : Consolidation
  priv min_known : Int32
}

///|
/// An archive accepts a bucket only when at least min_known samples are known.
pub fn Spec::new(
  factor : Int32,
  rows : Int32,
  aggregation : Consolidation,
  min_known~ : Int32 = 1,
) -> Spec raise ArchiveError {
  if factor < 1 ||
    factor > 1000000 ||
    rows < 1 ||
    rows > 1000000 ||
    min_known < 1 ||
    min_known > factor {
    raise InvalidConfig(
      "factor/rows must be 1..1000000; min_known must be 1..factor",
    )
  }
  { factor, rows, aggregation, min_known, }
}

///|
#derive(Debug, Eq)
pub struct Point {
  timestamp : Int64
  value : Double?
}

///|
struct Archive {
  spec : Spec
  data : Array[Point]
  mut head : Int32
  mut bucket : Int64
  mut count : Int32
  mut mean : Double
  mut low : Double
  mut high : Double
  mut last : Double
}

///|
pub struct Store {
  priv step : Int32
  priv archives : Array[Archive]
  priv mut latest : Int64
}

///|
fn finite(value : Double) -> Bool {
  !value.is_nan() && !value.is_inf()
}

///|
fn empty_archive(spec : Spec) -> Archive {
  {
    spec,
    data: [],
    head: 0,
    bucket: 0L,
    count: 0,
    mean: 0.0,
    low: 0.0,
    high: 0.0,
    last: 0.0,
  }
}

///|
/// Timestamps are positive UTC seconds, aligned to step, at interval ends.
pub fn Store::new(
  step : Int32,
  specs : Array[Spec],
) -> Store raise ArchiveError {
  if step < 1 || step > 86400 || specs.is_empty() || specs.length() > 32 {
    raise InvalidConfig("step must be 1..86400; archive count 1..32")
  }
  let mut total = 0
  for spec in specs {
    total += spec.rows
  }
  if total > 1000000 {
    raise InvalidConfig("total capacity exceeds one million points")
  }
  { step, archives: specs.map(empty_archive), latest: 0L, }
}

///|
fn Archive::push(self : Archive, point : Point) -> Unit {
  if self.data.length() < self.spec.rows {
    self.data.push(point)
  } else {
    self.data[self.head] = point
    self.head = (self.head + 1) % self.spec.rows
  }
}

///|
fn Archive::finish(self : Archive, step : Int32) -> Unit {
  let value = if self.count < self.spec.min_known {
    None
  } else {
    Some(
      match self.spec.aggregation {
        Average => self.mean
        Minimum => self.low
        Maximum => self.high
        Last => self.last
      },
    )
  }
  self.push({
    timestamp: (self.bucket + 1L) *
    self.spec.factor.to_int64() *
    step.to_int64(),
    value,
  })
  self.count = 0
  self.mean = 0.0
  self.low = 0.0
  self.high = 0.0
  self.last = 0.0
  self.bucket += 1L
}

///|
/// Input validation happens before any state is changed. A missing tick is unknown.
pub fn Store::update(
  self : Store,
  timestamp : Int64,
  value : Double?,
) -> Unit raise ArchiveError {
  if timestamp <= self.latest ||
    timestamp > 9000000000000000L ||
    timestamp % self.step.to_int64() != 0L {
    raise InvalidSample(
      "timestamp must increase, align to step, and be at most 9e15 seconds",
    )
  }
  if value is Some(v) && !finite(v) {
    raise InvalidSample("value must be finite")
  }
  let tick = timestamp / self.step.to_int64()
  for archive in self.archives {
    let target = (tick - 1L) / archive.spec.factor.to_int64()
    if self.latest == 0L {
      archive.bucket = target
    }
    if target > archive.bucket {
      archive.finish(self.step)
      let gap = target - archive.bucket
      if gap >= archive.spec.rows.to_int64() {
        archive.data.clear()
        archive.head = 0
        archive.bucket = target - archive.spec.rows.to_int64()
      }
      while archive.bucket < target {
        archive.finish(self.step)
      }
    }
    if value is Some(v) {
      let n = archive.count.to_double()
      archive.mean = archive.mean * (n / (n + 1.0)) + v / (n + 1.0)
      if archive.count == 0 {
        archive.low = v
        archive.high = v
      } else {
        if v < archive.low {
          archive.low = v
        }
        if v > archive.high {
          archive.high = v
        }
      }
      archive.last = v
      archive.count += 1
    }
    if tick % archive.spec.factor.to_int64() == 0L {
      archive.finish(self.step)
    }
  }
  self.latest = timestamp
}

///| Returns completed retained buckets in (start, end], ordered by timestamp.

///|
/// Missing buckets have None; overwritten/outside-history buckets are absent.
pub fn Store::fetch(
  self : Store,
  archive_index : Int32,
  start : Int64,
  end : Int64,
) -> Array[Point] raise ArchiveError {
  if archive_index < 0 || archive_index >= self.archives.length() || start > end {
    raise InvalidConfig("invalid archive index or range")
  }
  let archive = self.archives[archive_index]
  let result = []
  for i = 0; i < archive.data.length(); i = i + 1 {
    let point = archive.data[(archive.head + i) % archive.data.length()]
    if point.timestamp > start && point.timestamp <= end {
      result.push(point)
    }
  }
  result
}

///|
pub fn Store::latest(self : Store) -> Int64 {
  self.latest
}

///|
pub fn Store::retained(self : Store) -> Int32 {
  let mut total = 0
  for a in self.archives {
    total += a.data.length()
  }
  total
}