///|
/// Direction of a value series over a configured observation window.
pub(all) enum TrendDirection {
  RisingTrend
  FallingTrend
  StableTrend
  InsufficientTrend
} derive(Debug, Eq)

///|
/// Reason a sample was flagged by an analytics rule.
pub(all) enum AnomalyKind {
  UpperLimitAnomaly
  LowerLimitAnomaly
  SpikeAnomaly
  FlatlineAnomaly
  RateAnomaly
} derive(Debug, Eq)

///|
/// A timestamped value used by trend and anomaly calculations.
pub(all) struct AnalyticsSample {
  timestamp : Int
  value : Int
  quality : ReadingQuality
}

///|
pub fn AnalyticsSample::new(
  timestamp : Int,
  value : Int,
  quality? : ReadingQuality = Good,
) -> Result[AnalyticsSample, ModbusError] {
  if timestamp < 0 {
    Err(InvalidData)
  } else {
    Ok({ timestamp, value, quality })
  }
}

///|
pub fn AnalyticsSample::timestamp(self : AnalyticsSample) -> Int {
  self.timestamp
}

///|
pub fn AnalyticsSample::value(self : AnalyticsSample) -> Int {
  self.value
}

///|
pub fn AnalyticsSample::quality(self : AnalyticsSample) -> ReadingQuality {
  self.quality
}

///|
pub fn AnalyticsSample::usable(self : AnalyticsSample) -> Bool {
  self.quality == Good
}

///|
/// A bounded time series with deterministic order guarantees.
pub(all) struct AnalyticsWindow {
  id : String
  capacity : Int
  samples : Array[AnalyticsSample]
  mut dropped : Int
}

///|
pub fn AnalyticsWindow::new(
  id : String,
  capacity? : Int = 2048,
) -> Result[AnalyticsWindow, ModbusError] {
  if id == "" || capacity < 2 {
    Err(InvalidData)
  } else {
    Ok({ id, capacity, samples: [], dropped: 0 })
  }
}

///|
pub fn AnalyticsWindow::id(self : AnalyticsWindow) -> String {
  self.id
}

///|
pub fn AnalyticsWindow::capacity(self : AnalyticsWindow) -> Int {
  self.capacity
}

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

///|
pub fn AnalyticsWindow::dropped(self : AnalyticsWindow) -> Int {
  self.dropped
}

///|
pub fn AnalyticsWindow::append(
  self : AnalyticsWindow,
  sample : AnalyticsSample,
) -> Result[Unit, ModbusError] {
  if self.samples.length() > 0 &&
    sample.timestamp() < self.samples[self.samples.length() - 1].timestamp() {
    return Err(InvalidData)
  }
  if self.samples.length() >= self.capacity {
    ignore(self.samples.remove(0))
    self.dropped += 1
  }
  self.samples.push(sample)
  Ok(())
}

///|
pub fn AnalyticsWindow::at(
  self : AnalyticsWindow,
  index : Int,
) -> Result[AnalyticsSample, ModbusError] {
  if index < 0 || index >= self.samples.length() {
    Err(InvalidAddress)
  } else {
    Ok(self.samples[index])
  }
}

///|
pub fn AnalyticsWindow::first(self : AnalyticsWindow) -> AnalyticsSample? {
  if self.samples.length() == 0 {
    None
  } else {
    Some(self.samples[0])
  }
}

///|
pub fn AnalyticsWindow::last(self : AnalyticsWindow) -> AnalyticsSample? {
  if self.samples.length() == 0 {
    None
  } else {
    Some(self.samples[self.samples.length() - 1])
  }
}

///|
pub fn AnalyticsWindow::usable_values(self : AnalyticsWindow) -> Array[Int] {
  let result : Array[Int] = []
  for sample in self.samples {
    if sample.usable() {
      result.push(sample.value())
    }
  }
  result
}

///|
pub fn AnalyticsWindow::minimum(
  self : AnalyticsWindow,
) -> Result[Int, ModbusError] {
  let values = self.usable_values()
  if values.length() == 0 {
    return Err(Incomplete)
  }
  let mut result = values[0]
  for value in values {
    if value < result {
      result = value
    }
  }
  Ok(result)
}

///|
pub fn AnalyticsWindow::maximum(
  self : AnalyticsWindow,
) -> Result[Int, ModbusError] {
  let values = self.usable_values()
  if values.length() == 0 {
    return Err(Incomplete)
  }
  let mut result = values[0]
  for value in values {
    if value > result {
      result = value
    }
  }
  Ok(result)
}

///|
pub fn AnalyticsWindow::average(
  self : AnalyticsWindow,
) -> Result[Int, ModbusError] {
  let values = self.usable_values()
  if values.length() == 0 {
    return Err(Incomplete)
  }
  let mut sum = 0
  for value in values {
    sum += value
  }
  Ok(sum / values.length())
}

///|
pub fn AnalyticsWindow::range(
  self : AnalyticsWindow,
) -> Result[Int, ModbusError] {
  let low = match self.minimum() {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let high = match self.maximum() {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  Ok(high - low)
}

///|
pub fn AnalyticsWindow::slope(
  self : AnalyticsWindow,
) -> Result[Int, ModbusError] {
  if self.samples.length() < 2 {
    return Err(Incomplete)
  }
  let first = self.samples[0]
  let last = self.samples[self.samples.length() - 1]
  let elapsed = last.timestamp() - first.timestamp()
  if elapsed == 0 {
    return Err(InvalidData)
  }
  Ok((last.value() - first.value()) * 1000 / elapsed)
}

///|
pub fn AnalyticsWindow::direction(
  self : AnalyticsWindow,
  threshold : Int,
) -> Result[TrendDirection, ModbusError] {
  if threshold < 0 {
    return Err(InvalidData)
  }
  let slope = match self.slope() {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  if slope > threshold {
    Ok(RisingTrend)
  } else if slope < -threshold {
    Ok(FallingTrend)
  } else {
    Ok(StableTrend)
  }
}

///|
/// A limit and deviation rule applied to a series.
pub(all) struct AnalyticsRule {
  id : String
  lower : Int?
  upper : Int?
  spike_delta : Int
  flatline_delta : Int
  flatline_count : Int
}

///|
pub fn AnalyticsRule::new(
  id : String,
  lower? : Int = 0,
  upper? : Int = 0,
  spike_delta? : Int = 0,
  flatline_delta? : Int = 0,
  flatline_count? : Int = 0,
) -> Result[AnalyticsRule, ModbusError] {
  if id == "" || spike_delta < 0 || flatline_delta < 0 || flatline_count < 0 {
    return Err(InvalidData)
  }
  let selected_lower = if lower == 0 { None } else { Some(lower) }
  let selected_upper = if upper == 0 { None } else { Some(upper) }
  match (selected_lower, selected_upper) {
    (Some(low), Some(high)) => if low >= high { return Err(InvalidData) }
    _ => ()
  }
  Ok({
    id,
    lower: selected_lower,
    upper: selected_upper,
    spike_delta,
    flatline_delta,
    flatline_count,
  })
}

///|
pub fn AnalyticsRule::id(self : AnalyticsRule) -> String {
  self.id
}

///|
pub fn AnalyticsRule::lower(self : AnalyticsRule) -> Int? {
  self.lower
}

///|
pub fn AnalyticsRule::upper(self : AnalyticsRule) -> Int? {
  self.upper
}

///|
pub fn AnalyticsRule::spike_delta(self : AnalyticsRule) -> Int {
  self.spike_delta
}

///|
pub fn AnalyticsRule::flatline_delta(self : AnalyticsRule) -> Int {
  self.flatline_delta
}

///|
pub fn AnalyticsRule::flatline_count(self : AnalyticsRule) -> Int {
  self.flatline_count
}

///|
/// A detected deviation with enough context for an incident message.
pub(all) struct ValueAnomaly {
  rule_id : String
  timestamp : Int
  value : Int
  kind : AnomalyKind
  severity : Int
  detail : String
}

///|
pub fn ValueAnomaly::new(
  rule_id : String,
  timestamp : Int,
  value : Int,
  kind : AnomalyKind,
  severity : Int,
  detail : String,
) -> Result[ValueAnomaly, ModbusError] {
  if rule_id == "" ||
    timestamp < 0 ||
    severity < 0 ||
    severity > 100 ||
    detail == "" {
    return Err(InvalidData)
  }
  Ok({ rule_id, timestamp, value, kind, severity, detail })
}

///|
pub fn ValueAnomaly::rule_id(self : ValueAnomaly) -> String {
  self.rule_id
}

///|
pub fn ValueAnomaly::timestamp(self : ValueAnomaly) -> Int {
  self.timestamp
}

///|
pub fn ValueAnomaly::value(self : ValueAnomaly) -> Int {
  self.value
}

///|
pub fn ValueAnomaly::kind(self : ValueAnomaly) -> AnomalyKind {
  self.kind
}

///|
pub fn ValueAnomaly::severity(self : ValueAnomaly) -> Int {
  self.severity
}

///|
pub fn ValueAnomaly::detail(self : ValueAnomaly) -> String {
  self.detail
}

///|
pub fn anomaly_kind_name(kind : AnomalyKind) -> String {
  match kind {
    UpperLimitAnomaly => "upper-limit"
    LowerLimitAnomaly => "lower-limit"
    SpikeAnomaly => "spike"
    FlatlineAnomaly => "flatline"
    RateAnomaly => "rate"
  }
}

///|
pub fn AnalyticsRule::evaluate(
  self : AnalyticsRule,
  window : AnalyticsWindow,
) -> Array[ValueAnomaly] {
  let result : Array[ValueAnomaly] = []
  for i in 0.. value
      Err(_) => continue
    }
    match self.upper {
      Some(limit) =>
        if sample.value() > limit {
          let detail = "value=" +
            sample.value().to_string() +
            " upper=" +
            limit.to_string()
          match
            ValueAnomaly::new(
              self.id,
              sample.timestamp(),
              sample.value(),
              UpperLimitAnomaly,
              80,
              detail,
            ) {
            Ok(value) => result.push(value)
            Err(_) => ()
          }
        }
      None => ()
    }
    match self.lower {
      Some(limit) =>
        if sample.value() < limit {
          let detail = "value=" +
            sample.value().to_string() +
            " lower=" +
            limit.to_string()
          match
            ValueAnomaly::new(
              self.id,
              sample.timestamp(),
              sample.value(),
              LowerLimitAnomaly,
              80,
              detail,
            ) {
            Ok(value) => result.push(value)
            Err(_) => ()
          }
        }
      None => ()
    }
    if i > 0 {
      let previous = match window.at(i - 1) {
        Ok(value) => value
        Err(_) => continue
      }
      if self.spike_delta > 0 &&
        (sample.value() - previous.value()).abs() >= self.spike_delta {
        let detail = "delta=" +
          (sample.value() - previous.value()).abs().to_string()
        match
          ValueAnomaly::new(
            self.id,
            sample.timestamp(),
            sample.value(),
            SpikeAnomaly,
            70,
            detail,
          ) {
          Ok(value) => result.push(value)
          Err(_) => ()
        }
      }
    }
  }
  if self.flatline_count > 1 && window.length() >= self.flatline_count {
    let start = window.length() - self.flatline_count
    let baseline = match window.at(start) {
      Ok(value) => value.value()
      Err(_) => 0
    }
    let mut flat = true
    for i in start.. sample.value()
        Err(_) => baseline
      }
      if (value - baseline).abs() > self.flatline_delta {
        flat = false
      }
    }
    if flat {
      let timestamp = match window.last() {
        Some(value) => value.timestamp()
        None => 0
      }
      match
        ValueAnomaly::new(
          self.id,
          timestamp,
          baseline,
          FlatlineAnomaly,
          50,
          "unchanged-series",
        ) {
        Ok(value) => result.push(value)
        Err(_) => ()
      }
    }
  }
  result
}

///|
/// Fixed-width histogram used for distribution and percentile approximations.
pub(all) struct ValueHistogram {
  lower : Int
  upper : Int
  width : Int
  bins : Array[Int]
  mut outside : Int
}

///|
pub fn ValueHistogram::new(
  lower : Int,
  upper : Int,
  bin_count : Int,
) -> Result[ValueHistogram, ModbusError] {
  if lower >= upper || bin_count < 1 {
    return Err(InvalidData)
  }
  let width = (upper - lower + bin_count - 1) / bin_count
  let bins : Array[Int] = []
  for _ in 0.. Unit {
  if value < self.lower || value >= self.upper {
    self.outside += 1
  } else {
    let index = (value - self.lower) / self.width
    let safe = if index >= self.bins.length() {
      self.bins.length() - 1
    } else {
      index
    }
    self.bins[safe] += 1
  }
}

///|
pub fn ValueHistogram::bins(self : ValueHistogram) -> Array[Int] {
  self.bins
}

///|
pub fn ValueHistogram::outside(self : ValueHistogram) -> Int {
  self.outside
}

///|
pub fn ValueHistogram::total(self : ValueHistogram) -> Int {
  let mut sum = self.outside
  for count in self.bins {
    sum += count
  }
  sum
}

///|
pub fn ValueHistogram::percentile(
  self : ValueHistogram,
  percent : Int,
) -> Result[Int, ModbusError] {
  if percent < 0 || percent > 100 || self.total() == 0 {
    return Err(InvalidData)
  }
  let target = (self.total() * percent + 99) / 100
  let mut seen = 0
  for i in 0..= target {
      return Ok(self.lower + i * self.width)
    }
  }
  Ok(self.upper - 1)
}

///|
/// Combined trend and distribution report.
pub(all) struct AnalyticsReport {
  direction : TrendDirection
  average : Int
  minimum : Int
  maximum : Int
  slope : Int
  anomalies : Int
  p95 : Int
}

///|
pub fn analytics_report(
  window : AnalyticsWindow,
  rule : AnalyticsRule,
  histogram : ValueHistogram,
) -> Result[AnalyticsReport, ModbusError] {
  let average = match window.average() {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let minimum = match window.minimum() {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let maximum = match window.maximum() {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let slope = match window.slope() {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let direction = match window.direction(5) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let anomalies = rule.evaluate(window).length()
  let p95 = match histogram.percentile(95) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  Ok({ direction, average, minimum, maximum, slope, anomalies, p95 })
}

///|
pub fn trend_direction_name(direction : TrendDirection) -> String {
  match direction {
    RisingTrend => "rising"
    FallingTrend => "falling"
    StableTrend => "stable"
    InsufficientTrend => "insufficient"
  }
}

///|
pub fn AnalyticsReport::direction(self : AnalyticsReport) -> TrendDirection {
  self.direction
}

///|
pub fn AnalyticsReport::average(self : AnalyticsReport) -> Int {
  self.average
}

///|
pub fn AnalyticsReport::minimum(self : AnalyticsReport) -> Int {
  self.minimum
}

///|
pub fn AnalyticsReport::maximum(self : AnalyticsReport) -> Int {
  self.maximum
}

///|
pub fn AnalyticsReport::slope(self : AnalyticsReport) -> Int {
  self.slope
}

///|
pub fn AnalyticsReport::anomalies(self : AnalyticsReport) -> Int {
  self.anomalies
}

///|
pub fn AnalyticsReport::p95(self : AnalyticsReport) -> Int {
  self.p95
}