///|
/// Deterministic forecasting strategy for production planning.
pub(all) enum ForecastStrategy {
  LastValue
  MeanValue
  MovingAverage(Int)
  TrendLine
  ExponentialAverage(Double)
} derive(Debug, Eq, ToJson)

///|
/// A forecast plus uncertainty bands and evaluation metadata.
pub(all) struct ForecastResult {
  strategy_name : String
  horizon : Int
  values : Array[Double]
  baseline : Double
  deviation : Double?
  lower : Array[Double]
  upper : Array[Double]
} derive(Debug, ToJson)

///|
/// Produce a deterministic forecast for a future horizon.
pub fn forecast_with(
  values : Array[Double],
  strategy : ForecastStrategy,
  horizon : Int,
) -> ForecastResult? {
  if values.length() == 0 || horizon < 0 {
    None
  } else {
    let future : Array[Double]? = match strategy {
      LastValue => Some(repeat_forecast(values[values.length() - 1], horizon))
      MeanValue => Some(repeat_forecast(mean_of(values), horizon))
      MovingAverage(width) =>
        match moving_mean(values, width) {
          Some(result) =>
            if result.length() == 0 {
              None
            } else {
              Some(repeat_forecast(result[result.length() - 1], horizon))
            }
          None => None
        }
      TrendLine => forecast(values, horizon)
      ExponentialAverage(lambda) => {
        let result = ewma(values, lambda~, initial=values[0])
        if result.length() == 0 {
          None
        } else {
          Some(repeat_forecast(result[result.length() - 1], horizon))
        }
      }
    }
    match future {
      None => None
      Some(predictions) => {
        let errors = fitted_errors(values, strategy)
        let deviation = if errors.length() < 2 {
          None
        } else {
          Some(std_of(errors, mean_of(errors)))
        }
        let lower : Array[Double] = []
        let upper : Array[Double] = []
        for value in predictions {
          let margin = match deviation {
            Some(error) => 1.96 * error
            None => 0.0
          }
          lower.push(value - margin)
          upper.push(value + margin)
        }
        Some({
          strategy_name: strategy_name(strategy),
          horizon,
          values: predictions,
          baseline: values[values.length() - 1],
          deviation,
          lower,
          upper,
        })
      }
    }
  }
}

///|
fn repeat_forecast(value : Double, count : Int) -> Array[Double] {
  let result : Array[Double] = []
  for _ in 0.. String {
  match strategy {
    LastValue => "last-value"
    MeanValue => "mean"
    MovingAverage(_) => "moving-average"
    TrendLine => "trend"
    ExponentialAverage(_) => "exponential"
  }
}

///|
/// Fitted residuals for a strategy.
pub fn fitted_errors(
  values : Array[Double],
  strategy : ForecastStrategy,
) -> Array[Double] {
  let fitted : Array[Double] = []
  match strategy {
    LastValue =>
      for index in 0..
      for _ in values {
        fitted.push(mean_of(values))
      }
    MovingAverage(width) =>
      if width > 0 {
        for index in 0..
      match linear_trend(values) {
        Some(trend) =>
          for index in 0.. ()
      }
    ExponentialAverage(lambda) =>
      for value in ewma(values, lambda~, initial=values[0]) {
        fitted.push(value)
      }
  }
  let errors : Array[Double] = []
  for index in 0.. Double? {
  match paired_errors(actual, predicted) {
    None => None
    Some(errors) =>
      if errors.length() == 0 {
        None
      } else {
        let mut total = 0.0
        for error in errors {
          total += error.abs()
        }
        Some(total / errors.length().to_double())
      }
  }
}

///|
/// Mean squared forecast error.
pub fn mean_squared_error(
  actual : Array[Double],
  predicted : Array[Double],
) -> Double? {
  match paired_errors(actual, predicted) {
    None => None
    Some(errors) =>
      if errors.length() == 0 {
        None
      } else {
        let mut total = 0.0
        for error in errors {
          total += error * error
        }
        Some(total / errors.length().to_double())
      }
  }
}

///|
/// Root mean squared forecast error.
pub fn root_mean_squared_error(
  actual : Array[Double],
  predicted : Array[Double],
) -> Double? {
  match mean_squared_error(actual, predicted) {
    Some(value) => Some(value.sqrt())
    None => None
  }
}

///|
fn paired_errors(
  actual : Array[Double],
  predicted : Array[Double],
) -> Array[Double]? {
  if actual.length() != predicted.length() {
    None
  } else {
    let errors : Array[Double] = []
    for index in 0.. Double? {
  match paired_errors(actual, predicted) {
    None => None
    Some(errors) => {
      let mut total = 0.0
      let mut count = 0
      for index in 0.. 0.0 {
          total += 2.0 * errors[index].abs() / denominator
          count += 1
        }
      }
      if count == 0 {
        None
      } else {
        Some(total / count.to_double())
      }
    }
  }
}

///|
/// One-step historical backtest result.
pub(all) struct BacktestPoint {
  index : Int
  actual : Double
  predicted : Double
  error : Double
} derive(Debug, ToJson)

///|
/// Evaluate one-step forecasts using expanding history.
pub fn backtest_forecast(
  values : Array[Double],
  strategy : ForecastStrategy,
  minimum_history : Int,
) -> Array[BacktestPoint] {
  let result : Array[BacktestPoint] = []
  if minimum_history > 0 {
    for index in minimum_history.. {
          let error = values[index] - prediction.values[0]
          result.push({
            index,
            actual: values[index],
            predicted: prediction.values[0],
            error,
          })
        }
        None => ()
      }
    }
  }
  result
}

///|
/// Aggregate backtest errors into MAE and deviation.
pub fn backtest_metrics(points : Array[BacktestPoint]) -> (Double, Double)? {
  if points.length() == 0 {
    None
  } else {
    let errors : Array[Double] = []
    for point in points {
      errors.push(point.error)
    }
    Some((mean_of(errors), std_of(errors, mean_of(errors))))
  }
}

///|
/// Pick the lowest-MAE strategy from a candidate list.
pub fn choose_forecast_strategy(
  values : Array[Double],
  strategies : Array[ForecastStrategy],
  minimum_history : Int,
) -> ForecastStrategy? {
  let mut best : ForecastStrategy? = None
  let mut best_error = 0.0
  for strategy in strategies {
    let points = backtest_forecast(values, strategy, minimum_history)
    match backtest_metrics(points) {
      Some((mean, _)) => {
        let error = mean.abs()
        match best {
          None => {
            best = Some(strategy)
            best_error = error
          }
          Some(_) =>
            if error < best_error {
              best = Some(strategy)
              best_error = error
            }
        }
      }
      None => ()
    }
  }
  best
}

///|
/// Generate multiple uncertainty levels around one forecast.
pub fn forecast_bands(
  values : Array[Double],
  deviation : Double,
  levels : Array[Double],
) -> Array[(Array[Double], Array[Double])] {
  let result : Array[(Array[Double], Array[Double])] = []
  for level in levels {
    let lower : Array[Double] = []
    let upper : Array[Double] = []
    for value in values {
      let margin = level.abs() * deviation
      lower.push(value - margin)
      upper.push(value + margin)
    }
    result.push((lower, upper))
  }
  result
}