///|
/// Production-oriented time-series primitives for forecasting, monitoring, and
/// feature extraction. All transformations return owned arrays or immutable
/// records so callers can safely reuse the original stream.
pub enum Seasonality {
  None
  Additive(period~ : Int)
  Multiplicative(period~ : Int)
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct TimeSeries {
  timestamps : Array[Int]
  values : Array[Double]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn TimeSeries::new(
  timestamps : Array[Int],
  values : Array[Double],
) -> TimeSeries {
  if timestamps.length() != values.length() {
    abort("TimeSeries: timestamps and values must have equal length")
  }
  { timestamps: timestamps.copy(), values: values.copy() }
}

///|
pub fn TimeSeries::from_values(values : Array[Double]) -> TimeSeries {
  let timestamps = []
  for index in 0.. Int {
  self.values.length()
}

///|
pub fn TimeSeries::is_empty(self : TimeSeries) -> Bool {
  self.values.length() == 0
}

///|
pub fn TimeSeries::timestamps(self : TimeSeries) -> Array[Int] {
  self.timestamps.copy()
}

///|
pub fn TimeSeries::values(self : TimeSeries) -> Array[Double] {
  self.values.copy()
}

///|
pub fn TimeSeries::at(self : TimeSeries, index : Int) -> Double {
  if index < 0 || index >= self.len() {
    abort("TimeSeries::at: index out of range")
  }
  self.values[index]
}

///|
pub fn TimeSeries::timestamp_at(self : TimeSeries, index : Int) -> Int {
  if index < 0 || index >= self.len() {
    abort("TimeSeries::timestamp_at: index out of range")
  }
  self.timestamps[index]
}

///|
pub fn TimeSeries::append(
  self : TimeSeries,
  timestamp : Int,
  value : Double,
) -> TimeSeries {
  if self.len() > 0 && timestamp <= self.timestamps[self.len() - 1] {
    abort("TimeSeries::append: timestamps must increase")
  }
  let timestamps = self.timestamps.copy()
  timestamps.push(timestamp)
  let values = self.values.copy()
  values.push(value)
  { timestamps, values }
}

///|
pub fn TimeSeries::slice(
  self : TimeSeries,
  start : Int,
  finish : Int,
) -> TimeSeries {
  if start < 0 || finish < start || finish > self.len() {
    abort("TimeSeries::slice: invalid range")
  }
  let timestamps = []
  let values = []
  for index in start.. Double,
) -> TimeSeries {
  { ..self, values: self.values.map(transform) }
}

///|
pub fn TimeSeries::map_indexed(
  self : TimeSeries,
  transform : (Int, Double) -> Double,
) -> TimeSeries {
  { ..self, values: self.values.mapi(transform) }
}

///|
pub fn TimeSeries::filter(
  self : TimeSeries,
  predicate : (Int, Double) -> Bool,
) -> TimeSeries {
  let timestamps = []
  let values = []
  for index, value in self.values {
    if predicate(self.timestamps[index], value) {
      timestamps.push(self.timestamps[index])
      values.push(value)
    }
  }
  { timestamps, values }
}

///|
pub fn TimeSeries::difference(self : TimeSeries, lag : Int) -> TimeSeries {
  if lag <= 0 || lag >= self.len() {
    abort("TimeSeries::difference: lag out of range")
  }
  let timestamps = []
  let values = []
  for index in lag.. TimeSeries {
  if lag <= 0 || lag >= self.len() {
    abort("TimeSeries::percentage_change: lag out of range")
  }
  let timestamps = []
  let values = []
  for index in lag.. TimeSeries {
  if offset < 0 || offset >= self.len() {
    abort("TimeSeries::lag: offset out of range")
  }
  let timestamps = []
  let values = []
  for index in offset.. Double,
) -> TimeSeries {
  if window <= 0 || window > self.len() {
    abort("TimeSeries::rolling: invalid window")
  }
  let timestamps = []
  let values = []
  for index in (window - 1).. TimeSeries {
  self.rolling(window, mean)
}

///|
pub fn TimeSeries::rolling_variance(
  self : TimeSeries,
  window : Int,
) -> TimeSeries {
  self.rolling(window, variance)
}

///|
pub fn TimeSeries::rolling_min(self : TimeSeries, window : Int) -> TimeSeries {
  self.rolling(window, fn(values) {
    values.fold(init=values[0], (current, value) => current.min(value))
  })
}

///|
pub fn TimeSeries::rolling_max(self : TimeSeries, window : Int) -> TimeSeries {
  self.rolling(window, fn(values) {
    values.fold(init=values[0], (current, value) => current.max(value))
  })
}

///|
pub fn TimeSeries::resample_mean(self : TimeSeries, bucket : Int) -> TimeSeries {
  if bucket <= 0 || self.len() == 0 {
    abort("TimeSeries::resample_mean: invalid bucket")
  }
  let timestamps = []
  let values = []
  let mut index = 0
  while index < self.len() {
    let finish = (index + bucket).min(self.len())
    let sample = []
    for cursor in index.. TimeSeries {
  if bucket <= 0 || self.len() == 0 {
    abort("TimeSeries::resample_sum: invalid bucket")
  }
  let timestamps = []
  let values = []
  let mut index = 0
  while index < self.len() {
    let finish = (index + bucket).min(self.len())
    let sample = []
    for cursor in index.. TimeSeries {
  if self.len() == 0 {
    return self
  }
  let lower = self.values.fold(init=self.values[0], (current, value) => {
    current.min(value)
  })
  let upper = self.values.fold(init=self.values[0], (current, value) => {
    current.max(value)
  })
  let width = upper - lower
  self.map(fn(value) {
    if width == 0.0 {
      0.0
    } else {
      (value - lower) / width
    }
  })
}

///|
pub fn TimeSeries::standardize(self : TimeSeries) -> TimeSeries {
  let center = mean(self.values)
  let scale = variance(self.values).sqrt()
  self.map(fn(value) {
    if scale == 0.0 {
      0.0
    } else {
      (value - center) / scale
    }
  })
}

///|
pub fn TimeSeries::trend(self : TimeSeries) -> Double {
  if self.len() < 2 {
    return 0.0
  }
  let x = []
  for index in 0.. Array[Double] {
  if period <= 0 || self.len() < period {
    abort("TimeSeries::seasonal_average: invalid period")
  }
  let result = Array::make(period, 0.0)
  let counts = Array::make(period, 0)
  for index, value in self.values {
    let slot = index % period
    result[slot] = result[slot] + value
    counts[slot] = counts[slot] + 1
  }
  for slot in 0.. Double {
  if lag < 0 || lag >= values.length() || values.length() < 2 {
    abort("autocovariance: invalid lag")
  }
  let center = mean(values)
  let mut result = 0.0
  for index in lag.. Array[Double] {
  if maximum_lag < 0 || maximum_lag >= values.length() {
    abort("autocorrelation_series: invalid lag")
  }
  let result = []
  let base = autocovariance(values, 0)
  for lag in 0..<=maximum_lag {
    result.push(
      if base == 0.0 {
        0.0
      } else {
        autocovariance(values, lag) / base
      },
    )
  }
  result
}

///|
pub fn partial_autocorrelation(values : Array[Double], lag : Int) -> Double {
  if lag < 0 || lag >= values.length() {
    abort("partial_autocorrelation: invalid lag")
  }
  if lag == 0 {
    return 1.0
  }
  let correlations = autocorrelation_series(values, lag)
  let mut coefficient = correlations[lag]
  for index in 1.. TrendEstimate {
  { slope, intercept, r_squared }
}

///|
pub fn TrendEstimate::slope(self : TrendEstimate) -> Double {
  self.slope
}

///|
pub fn TrendEstimate::intercept(self : TrendEstimate) -> Double {
  self.intercept
}

///|
pub fn TrendEstimate::r_squared(self : TrendEstimate) -> Double {
  self.r_squared
}

///|
pub fn TrendEstimate::predict(self : TrendEstimate, x : Double) -> Double {
  self.intercept + self.slope * x
}

///|
pub fn fit_trend(values : Array[Double]) -> TrendEstimate {
  if values.length() < 2 {
    abort("fit_trend: at least two values are required")
  }
  let x = []
  for index in 0.. {
    result + (value - mean(values)) * (value - mean(values))
  })
  let residual = mean_absolute_error(predictions, values)
  TrendEstimate::new(
    slope,
    intercept,
    if total == 0.0 {
      0.0
    } else {
      1.0 - residual * residual * values.length().to_double() / total
    },
  )
}

///|
pub fn detrend(values : Array[Double]) -> Array[Double] {
  let trend = fit_trend(values)
  values.mapi(fn(index, value) { value - trend.predict(index.to_double()) })
}

///|
pub fn add_trend(
  values : Array[Double],
  trend : TrendEstimate,
) -> Array[Double] {
  values.mapi(fn(index, value) { value + trend.predict(index.to_double()) })
}

///|
pub fn seasonal_lag_features(
  values : Array[Double],
  period : Int,
  lags : Int,
) -> Array[Array[Double]] {
  if period <= 0 || lags <= 0 || values.length() <= period * lags {
    abort("seasonal_lag_features: insufficient history")
  }
  let result = []
  let start = period * lags
  for index in start.. Array[Double] {
  if window <= 0 || window > values.length() {
    abort("rolling_quantile: invalid window")
  }
  let result = []
  for index in (window - 1).. Array[Double] {
  rolling_quantile(values, window, 0.5)
}

///|
pub fn rolling_iqr(values : Array[Double], window : Int) -> Array[Double] {
  if window <= 0 || window > values.length() {
    abort("rolling_iqr: invalid window")
  }
  let result = []
  for index in (window - 1).. Array[Double] {
  if values.length() == 0 || decay <= 0.0 || decay > 1.0 || horizon < 0 {
    abort("ewma_forecast: invalid inputs")
  }
  let level = exponential_moving_average(values, decay)[values.length() - 1]
  Array::make(horizon, level)
}

///|
pub fn holt_linear_forecast(
  values : Array[Double],
  level_decay : Double,
  trend_decay : Double,
  horizon : Int,
) -> Array[Double] {
  if values.length() < 2 ||
    level_decay <= 0.0 ||
    level_decay > 1.0 ||
    trend_decay <= 0.0 ||
    trend_decay > 1.0 ||
    horizon < 0 {
    abort("holt_linear_forecast: invalid inputs")
  }
  let mut level = values[0]
  let mut trend = values[1] - values[0]
  for index in 1.. Array[Double] {
  if damping <= 0.0 || damping > 1.0 {
    abort("damped_trend_forecast: damping must be in (0, 1]")
  }
  let base = holt_linear_forecast(values, level_decay, trend_decay, horizon)
  let result = []
  for index, value in base {
    result.push(value * @math.pow(damping, (index + 1).to_double()))
  }
  result
}

///|
pub fn forecast_bias(
  actual : Array[Double],
  predicted : Array[Double],
) -> Double {
  mean_absolute_error(actual, predicted)
}

///|
pub fn forecast_mase(
  actual : Array[Double],
  predicted : Array[Double],
  training : Array[Double],
  seasonal_period : Int,
) -> Double {
  if actual.length() != predicted.length() ||
    training.length() <= seasonal_period {
    abort("forecast_mase: invalid lengths")
  }
  let first = []
  let second = []
  for index in seasonal_period.. Double {
  if actual.length() != predicted.length() || actual.length() == 0 {
    abort("forecast_smape: invalid lengths")
  }
  let mut total = 0.0
  for index in 0..