///|
/// Qualitative stability of a history-based storage projection.
pub(all) enum ForecastConfidence {
  Insufficient
  Low
  Medium
  High
} derive(Eq, Debug, ToJson, FromJson)

///|
/// One projected storage observation at a future timestamp.
pub(all) struct StorageProjection {
  created_at : Int64
  projected_bytes : Int64
} derive(Eq, Debug, ToJson, FromJson)

///|
/// Deterministic linear projection derived from snapshot history.
pub(all) struct StorageForecast {
  observations : Int
  horizon_days : Int
  current_bytes : Int64
  daily_growth_bytes : Int64
  volatility_bytes : Int64
  projected_bytes : Int64
  confidence : ForecastConfidence
  projections : Array[StorageProjection]
} derive(Eq, Debug, ToJson, FromJson)

///|
/// Result of estimating when a configured capacity will be reached.
pub(all) struct CapacityEstimate {
  capacity_bytes : Int64
  current_bytes : Int64
  daily_growth_bytes : Int64
  days_remaining : Int?
  estimated_at : Int64?
  already_exceeded : Bool
  growing : Bool
} derive(Eq, Debug, ToJson, FromJson)

///|
fn forecast_abs(value : Int64) -> Int64 {
  if value < 0L {
    -value
  } else {
    value
  }
}

///|
fn history_daily_rates(history : SnapshotHistory) -> Array[Int64] {
  let rates : Array[Int64] = []
  if history.snapshots.length() < 2 {
    return rates
  }
  for index = 1; index < history.snapshots.length(); index = index + 1 {
    let previous = history.snapshots[index - 1]
    let current = history.snapshots[index]
    let elapsed = current.created_at - previous.created_at
    if elapsed > 0L {
      let bytes = current.total_size() - previous.total_size()
      rates.push(bytes * 86400L / elapsed)
    }
  }
  rates
}

///|
fn average_int64(values : Array[Int64]) -> Int64 {
  if values.is_empty() {
    return 0L
  }
  let mut total = 0L
  for value in values {
    total += value
  }
  total / values.length().to_int64()
}

///|
fn rate_volatility(rates : Array[Int64], average : Int64) -> Int64 {
  if rates.is_empty() {
    return 0L
  }
  let deviations = rates.map(fn(rate) { forecast_abs(rate - average) })
  average_int64(deviations)
}

///|
fn forecast_confidence(
  observations : Int,
  average : Int64,
  volatility : Int64,
) -> ForecastConfidence {
  if observations < 2 {
    Insufficient
  } else if observations < 4 {
    Low
  } else {
    let baseline = forecast_abs(average)
    if baseline == 0L {
      if volatility == 0L {
        High
      } else {
        Low
      }
    } else if volatility * 4L <= baseline {
      High
    } else if volatility <= baseline {
      Medium
    } else {
      Low
    }
  }
}

///|
pub fn forecast_confidence_name(confidence : ForecastConfidence) -> String {
  match confidence {
    Insufficient => "insufficient"
    Low => "low"
    Medium => "medium"
    High => "high"
  }
}

///|
fn non_negative_bytes(value : Int64) -> Int64 {
  if value < 0L {
    0L
  } else {
    value
  }
}

///|
/// Project one point per future day using the mean rate between actual
/// observations. No wall-clock access or floating-point rounding is involved.
pub fn forecast_storage(
  history : SnapshotHistory,
  horizon_days : Int,
) -> StorageForecast raise {
  if horizon_days < 0 {
    raise Failure("Forecast horizon cannot be negative")
  }
  let observations = history.snapshots.length()
  let current = if observations == 0 {
    0L
  } else {
    history.snapshots[observations - 1].total_size()
  }
  let latest_at = if observations == 0 {
    0L
  } else {
    history.snapshots[observations - 1].created_at
  }
  let rates = history_daily_rates(history)
  let daily_growth = average_int64(rates)
  let volatility = rate_volatility(rates, daily_growth)
  let projections : Array[StorageProjection] = []
  for day = 1; day <= horizon_days; day = day + 1 {
    projections.push(StorageProjection::{
      created_at: latest_at + day.to_int64() * 86400L,
      projected_bytes: non_negative_bytes(
        current + daily_growth * day.to_int64(),
      ),
    })
  }
  StorageForecast::{
    observations,
    horizon_days,
    current_bytes: current,
    daily_growth_bytes: daily_growth,
    volatility_bytes: volatility,
    projected_bytes: if projections.is_empty() {
      current
    } else {
      projections[projections.length() - 1].projected_bytes
    },
    confidence: forecast_confidence(observations, daily_growth, volatility),
    projections,
  }
}

///|
pub fn estimate_capacity(
  history : SnapshotHistory,
  capacity_bytes : Int64,
) -> CapacityEstimate raise {
  if capacity_bytes <= 0L {
    raise Failure("Capacity must be greater than zero")
  }
  let forecast = forecast_storage(history, 0)
  let latest_at = if history.snapshots.is_empty() {
    0L
  } else {
    history.snapshots[history.snapshots.length() - 1].created_at
  }
  if forecast.current_bytes >= capacity_bytes {
    CapacityEstimate::{
      capacity_bytes,
      current_bytes: forecast.current_bytes,
      daily_growth_bytes: forecast.daily_growth_bytes,
      days_remaining: Some(0),
      estimated_at: Some(latest_at),
      already_exceeded: true,
      growing: forecast.daily_growth_bytes > 0L,
    }
  } else if forecast.daily_growth_bytes <= 0L {
    CapacityEstimate::{
      capacity_bytes,
      current_bytes: forecast.current_bytes,
      daily_growth_bytes: forecast.daily_growth_bytes,
      days_remaining: None,
      estimated_at: None,
      already_exceeded: false,
      growing: false,
    }
  } else {
    let remaining = capacity_bytes - forecast.current_bytes
    let days = ((remaining + forecast.daily_growth_bytes - 1L) /
    forecast.daily_growth_bytes).to_int()
    CapacityEstimate::{
      capacity_bytes,
      current_bytes: forecast.current_bytes,
      daily_growth_bytes: forecast.daily_growth_bytes,
      days_remaining: Some(days),
      estimated_at: Some(latest_at + days.to_int64() * 86400L),
      already_exceeded: false,
      growing: true,
    }
  }
}

///|
pub fn render_storage_forecast(
  forecast : StorageForecast,
  capacity? : CapacityEstimate? = None,
) -> String {
  let lines : Array[String] = [
    "MoonDisk storage forecast",
    "Observations: " + forecast.observations.to_string(),
    "Confidence: " + forecast_confidence_name(forecast.confidence),
    "Current size: " + format_bytes(forecast.current_bytes),
    "Average daily growth: " + format_bytes(forecast.daily_growth_bytes),
    "Rate volatility: " + format_bytes(forecast.volatility_bytes),
    "Projected size after " +
    forecast.horizon_days.to_string() +
    " days: " +
    format_bytes(forecast.projected_bytes),
  ]
  match capacity {
    None => ()
    Some(estimate) => {
      lines.push("Capacity: " + format_bytes(estimate.capacity_bytes))
      if estimate.already_exceeded {
        lines.push("Capacity status: already exceeded")
      } else {
        match estimate.days_remaining {
          None => lines.push("Capacity status: no exhaustion projected")
          Some(days) =>
            lines.push(
              "Capacity status: estimated in " + days.to_string() + " days",
            )
        }
      }
    }
  }
  lines.join("\n")
}

///|
pub fn storage_forecast_to_json(forecast : StorageForecast) -> String {
  ToJson::to_json(forecast).stringify(indent=2)
}

///|
pub fn capacity_estimate_to_json(estimate : CapacityEstimate) -> String {
  ToJson::to_json(estimate).stringify(indent=2)
}