///|
/// Streaming forecasting models for operational demand and metric series.
///
/// The models expose both point forecasts and uncertainty bands.  They update
/// in O(1) or O(period) per observation, which keeps them useful for service
/// health metrics, inventory signals, and low-latency baseline prediction.
pub struct ForecastPoint {
  timestamp : Int64
  value : Double
  lower : Double
  upper : Double
}

///|
pub fn ForecastPoint::new(
  timestamp : Int64,
  value : Double,
  lower : Double,
  upper : Double,
) -> ForecastPoint {
  { timestamp, value, lower, upper }
}

///|
pub fn ForecastPoint::timestamp(self : ForecastPoint) -> Int64 {
  self.timestamp
}

///|
pub fn ForecastPoint::value(self : ForecastPoint) -> Double {
  self.value
}

///|
pub fn ForecastPoint::lower(self : ForecastPoint) -> Double {
  self.lower
}

///|
pub fn ForecastPoint::upper(self : ForecastPoint) -> Double {
  self.upper
}

///|
pub struct OnlineHoltWinters {
  period : Int
  alpha : Double
  beta : Double
  gamma : Double
  mut level : Double
  mut trend : Double
  seasonals : Array[Double]
  errors : OnlineQuantileSketch
  mut steps : Int
  mut initialized : Bool
}

///|
pub fn OnlineHoltWinters::new(
  period : Int,
  alpha? : Double = 0.2,
  beta? : Double = 0.05,
  gamma? : Double = 0.1,
  error_capacity? : Int = 256,
) -> OnlineHoltWinters {
  let safe_period = if period < 1 { 1 } else { period }
  {
    period: safe_period,
    alpha: clamp(alpha, 0.001, 1.0),
    beta: clamp(beta, 0.001, 1.0),
    gamma: clamp(gamma, 0.001, 1.0),
    level: 0.0,
    trend: 0.0,
    seasonals: Array::make(safe_period, 0.0),
    errors: OnlineQuantileSketch::new(capacity=error_capacity),
    steps: 0,
    initialized: false,
  }
}

///|
pub fn OnlineHoltWinters::update(
  self : OnlineHoltWinters,
  value : Double,
) -> Double {
  let observation = if value.is_nan() || value.is_inf() { 0.0 } else { value }
  if !self.initialized {
    self.level = observation
    self.trend = 0.0
    self.initialized = true
    self.steps = 1
    observation
  } else {
    let index = self.steps % self.period
    let previous_level = self.level
    let seasonal = self.seasonals[index]
    self.level = self.alpha * (observation - seasonal) +
      (1.0 - self.alpha) * (self.level + self.trend)
    self.trend = self.beta * (self.level - previous_level) +
      (1.0 - self.beta) * self.trend
    self.seasonals[index] = self.gamma * (observation - self.level) +
      (1.0 - self.gamma) * seasonal
    let residual = observation - (previous_level + self.trend + seasonal)
    self.errors.update(residual.abs())
    self.steps += 1
    self.level + self.trend + seasonal
  }
}

///|
pub fn OnlineHoltWinters::predict(
  self : OnlineHoltWinters,
  horizon : Int,
  start_timestamp? : Int64 = 0L,
  step? : Int64 = 1L,
) -> Array[ForecastPoint] {
  let count = if horizon < 0 { 0 } else { horizon }
  let result : Array[ForecastPoint] = []
  let spread = self.errors.quantile(0.9).unwrap_or(0.0)
  for i in 0.. Double {
  self.level
}

///|
pub fn OnlineHoltWinters::trend(self : OnlineHoltWinters) -> Double {
  self.trend
}

///|
pub fn OnlineHoltWinters::period(self : OnlineHoltWinters) -> Int {
  self.period
}

///|
pub fn OnlineHoltWinters::steps(self : OnlineHoltWinters) -> Int {
  self.steps
}

///|
pub fn OnlineHoltWinters::residual_scale(self : OnlineHoltWinters) -> Double {
  self.errors.quantile(0.5).unwrap_or(0.0)
}

///|
pub fn OnlineHoltWinters::reset(self : OnlineHoltWinters) -> Unit {
  self.level = 0.0
  self.trend = 0.0
  self.seasonals.fill(0.0)
  self.errors.reset()
  self.steps = 0
  self.initialized = false
}

///|
/// Croston-style intermittent demand forecaster.  It separately smooths
/// non-zero demand size and the interval between non-zero arrivals.
pub struct IntermittentDemandForecaster {
  alpha : Double
  mut demand : Double
  mut interval : Double
  mut since_demand : Int
  mut observations : Int
  mut nonzero : Int
}

///|
pub fn IntermittentDemandForecaster::new(
  alpha? : Double = 0.1,
) -> IntermittentDemandForecaster {
  {
    alpha: clamp(alpha, 0.001, 1.0),
    demand: 0.0,
    interval: 1.0,
    since_demand: 0,
    observations: 0,
    nonzero: 0,
  }
}

///|
pub fn IntermittentDemandForecaster::update(
  self : IntermittentDemandForecaster,
  value : Double,
) -> Double {
  let observation = if value.is_nan() || value.is_inf() || value < 0.0 {
    0.0
  } else {
    value
  }
  self.observations += 1
  self.since_demand += 1
  if observation > 0.0 {
    if self.nonzero == 0 {
      self.demand = observation
      self.interval = self.since_demand.to_double()
    } else {
      self.demand = self.alpha * observation + (1.0 - self.alpha) * self.demand
      self.interval = self.alpha * self.since_demand.to_double() +
        (1.0 - self.alpha) * self.interval
    }
    self.since_demand = 0
    self.nonzero += 1
  }
  self.forecast()
}

///|
pub fn IntermittentDemandForecaster::forecast(
  self : IntermittentDemandForecaster,
) -> Double {
  if self.interval <= 0.0 {
    0.0
  } else {
    self.demand / self.interval
  }
}

///|
pub fn IntermittentDemandForecaster::demand_size(
  self : IntermittentDemandForecaster,
) -> Double {
  self.demand
}

///|
pub fn IntermittentDemandForecaster::interval(
  self : IntermittentDemandForecaster,
) -> Double {
  self.interval
}

///|
pub fn IntermittentDemandForecaster::observations(
  self : IntermittentDemandForecaster,
) -> Int {
  self.observations
}

///|
pub fn IntermittentDemandForecaster::nonzero(
  self : IntermittentDemandForecaster,
) -> Int {
  self.nonzero
}

///|
pub fn IntermittentDemandForecaster::predict(
  self : IntermittentDemandForecaster,
  horizon : Int,
  start_timestamp? : Int64 = 0L,
  step? : Int64 = 1L,
) -> Array[ForecastPoint] {
  let result : Array[ForecastPoint] = []
  let count = if horizon < 0 { 0 } else { horizon }
  let value = self.forecast()
  for i in 0.. Unit {
  self.demand = 0.0
  self.interval = 1.0
  self.since_demand = 0
  self.observations = 0
  self.nonzero = 0
}

///|
pub struct SeasonalNaiveBaseline {
  period : Int
  history : Array[Double]
  mut observations : Int
}

///|
pub fn SeasonalNaiveBaseline::new(period : Int) -> SeasonalNaiveBaseline {
  { period: if period < 1 { 1 } else { period }, history: [], observations: 0 }
}

///|
pub fn SeasonalNaiveBaseline::update(
  self : SeasonalNaiveBaseline,
  value : Double,
) -> Double {
  let safe = if value.is_nan() || value.is_inf() { 0.0 } else { value }
  let forecast = self.forecast()
  self.history.push(safe)
  if self.history.length() > self.period {
    let _ = self.history.remove(0)
  }
  self.observations += 1
  forecast
}

///|
pub fn SeasonalNaiveBaseline::forecast(self : SeasonalNaiveBaseline) -> Double {
  match self.history.last() {
    Some(value) => value
    None => 0.0
  }
}

///|
pub fn SeasonalNaiveBaseline::predict(
  self : SeasonalNaiveBaseline,
  horizon : Int,
  start_timestamp? : Int64 = 0L,
  step? : Int64 = 1L,
) -> Array[ForecastPoint] {
  let result : Array[ForecastPoint] = []
  let count = if horizon < 0 { 0 } else { horizon }
  let value = self.forecast()
  for i in 0.. Int {
  self.period
}

///|
pub fn SeasonalNaiveBaseline::observations(self : SeasonalNaiveBaseline) -> Int {
  self.observations
}

///|
pub fn SeasonalNaiveBaseline::history(
  self : SeasonalNaiveBaseline,
) -> Array[Double] {
  copy_vector(self.history)
}

///|
pub fn SeasonalNaiveBaseline::reset(self : SeasonalNaiveBaseline) -> Unit {
  self.history.clear()
  self.observations = 0
}

///|
pub struct ForecastErrorTracker {
  mut observations : Int
  mut absolute_error : Double
  mut squared_error : Double
  mut percentage_error : Double
  mut bias : Double
  mut underestimates : Int
  mut overestimates : Int
  mut worst_error : Double
}

///|
pub fn ForecastErrorTracker::new() -> ForecastErrorTracker {
  {
    observations: 0,
    absolute_error: 0.0,
    squared_error: 0.0,
    percentage_error: 0.0,
    bias: 0.0,
    underestimates: 0,
    overestimates: 0,
    worst_error: 0.0,
  }
}

///|
pub fn ForecastErrorTracker::observe(
  self : ForecastErrorTracker,
  actual : Double,
  forecast : Double,
) -> Unit {
  if actual.is_nan() ||
    forecast.is_nan() ||
    actual.is_inf() ||
    forecast.is_inf() {
    ()
  } else {
    let error = actual - forecast
    let absolute = error.abs()
    self.observations += 1
    self.absolute_error += absolute
    self.squared_error += error * error
    self.bias += error
    if actual.abs() > 1.0e-12 {
      self.percentage_error += absolute / actual.abs()
    }
    if error >= 0.0 {
      self.underestimates += 1
    } else {
      self.overestimates += 1
    }
    if absolute > self.worst_error {
      self.worst_error = absolute
    }
  }
}

///|
pub fn ForecastErrorTracker::observations(self : ForecastErrorTracker) -> Int {
  self.observations
}

///|
pub fn ForecastErrorTracker::mae(self : ForecastErrorTracker) -> Double {
  if self.observations == 0 {
    0.0
  } else {
    self.absolute_error / self.observations.to_double()
  }
}

///|
pub fn ForecastErrorTracker::rmse(self : ForecastErrorTracker) -> Double {
  if self.observations == 0 {
    0.0
  } else {
    (self.squared_error / self.observations.to_double()).sqrt()
  }
}

///|
pub fn ForecastErrorTracker::mape(self : ForecastErrorTracker) -> Double {
  if self.observations == 0 {
    0.0
  } else {
    self.percentage_error / self.observations.to_double()
  }
}

///|
pub fn ForecastErrorTracker::bias(self : ForecastErrorTracker) -> Double {
  if self.observations == 0 {
    0.0
  } else {
    self.bias / self.observations.to_double()
  }
}

///|
pub fn ForecastErrorTracker::underestimates(self : ForecastErrorTracker) -> Int {
  self.underestimates
}

///|
pub fn ForecastErrorTracker::overestimates(self : ForecastErrorTracker) -> Int {
  self.overestimates
}

///|
pub fn ForecastErrorTracker::worst_error(self : ForecastErrorTracker) -> Double {
  self.worst_error
}

///|
pub fn ForecastErrorTracker::reset(self : ForecastErrorTracker) -> Unit {
  self.observations = 0
  self.absolute_error = 0.0
  self.squared_error = 0.0
  self.percentage_error = 0.0
  self.bias = 0.0
  self.underestimates = 0
  self.overestimates = 0
  self.worst_error = 0.0
}

///|
pub struct ForecastBlendModel {
  weights : Map[String, Double]
  forecasts : Map[String, Array[ForecastPoint]]
  mut updates : Int
}

///|
pub fn ForecastBlendModel::new() -> ForecastBlendModel {
  { weights: Map([]), forecasts: Map([]), updates: 0 }
}

///|
pub fn ForecastBlendModel::set_weight(
  self : ForecastBlendModel,
  model : String,
  weight : Double,
) -> Bool {
  if model == "" {
    false
  } else {
    self.weights[model] = if weight < 0.0 { 0.0 } else { weight }
    true
  }
}

///|
pub fn ForecastBlendModel::observe(
  self : ForecastBlendModel,
  model : String,
  points : Array[ForecastPoint],
) -> Bool {
  if model == "" {
    false
  } else {
    self.forecasts[model] = points.map(point => point)
    self.updates += 1
    true
  }
}

///|
pub fn ForecastBlendModel::predict(
  self : ForecastBlendModel,
) -> Array[ForecastPoint] {
  if self.forecasts.is_empty() {
    []
  } else {
    let first = self.forecasts.values().next()
    match first {
      None => []
      Some(points) => {
        let result = Array::makei(points.length(), i => {
          ForecastPoint::new(points[i].timestamp(), 0.0, 0.0, 0.0)
        })
        let weight_sum = Ref(0.0)
        self.forecasts.each((model, values) => {
          let weight = self.weights.get(model).unwrap_or(1.0)
          weight_sum.val += weight
          let limit = if values.length() < result.length() {
            values.length()
          } else {
            result.length()
          }
          for i in 0.. 0.0 {
          for i in 0.. Double {
  self.weights.get(model).unwrap_or(0.0)
}

///|
pub fn ForecastBlendModel::models(self : ForecastBlendModel) -> Array[String] {
  let result : Array[String] = []
  self.forecasts.each((model, _) => result.push(model))
  result
}

///|
pub fn ForecastBlendModel::updates(self : ForecastBlendModel) -> Int {
  self.updates
}

///|
pub fn ForecastBlendModel::reset(self : ForecastBlendModel) -> Unit {
  self.forecasts.clear()
  self.weights.clear()
  self.updates = 0
}