///|
/// Advanced time-series state used for reusable decomposition and seasonality diagnostics.
pub struct TimeSeriesPoint {
  index : Int
  value : Double
  trend : Double
  seasonal : Double
  residual : Double
}

///|
pub struct TimeSeriesSummary {
  count : Int
  period : Int
  trend_strength : Double
  seasonal_strength : Double
  residual_scale : Double
  forecastability : Double
}

///|
pub struct TimeSeriesWindow {
  start : Int
  end : Int
  center : Double
  scale : Double
  slope : Double
}

///|
fn timeseries_regression_slope(
  values : Array[Double],
  x_values : Array[Double],
) -> Double {
  let x_center = mean(x_values)
  let y_center = mean(values)
  let mut numerator = 0.0
  let mut denominator = 0.0
  for index = 0
      index < values.length() && index < x_values.length()
      index = index + 1 {
    let dx = x_values[index] - x_center
    numerator += dx * (values[index] - y_center)
    denominator += dx * dx
  }
  if denominator == 0.0 {
    0.0
  } else {
    numerator / denominator
  }
}

///|
fn timeseries_component_at(
  data : Array[Double],
  period : Int,
  index : Int,
) -> Double {
  let safe_period = if period < 2 { 2 } else { period }
  let phase = index % safe_period
  let values = []
  for cursor = phase; cursor < data.length(); cursor = cursor + safe_period {
    values.push(data[cursor])
  }
  mean(values) - mean(data)
}

///|
pub fn timeseries_points(
  data : Array[Double],
  period : Int,
  window : Int,
) -> Array[TimeSeriesPoint] {
  let result = []
  let decomposition = seasonal_decompose(data, period, window)
  for index = 0; index < data.length(); index = index + 1 {
    let trend = if index < decomposition.trend.length() {
      decomposition.trend[index]
    } else {
      data[index]
    }
    let seasonal = if index < decomposition.seasonal.length() {
      decomposition.seasonal[index]
    } else {
      0.0
    }
    let residual = data[index] - trend - seasonal
    result.push({ index, value: data[index], trend, seasonal, residual })
  }
  result
}

///|
pub fn timeseries_summary(
  data : Array[Double],
  period : Int,
  window : Int,
) -> TimeSeriesSummary {
  let points = timeseries_points(data, period, window)
  let trend_values = []
  let seasonal_values = []
  let residual_values = []
  for point in points {
    trend_values.push(point.trend)
    seasonal_values.push(point.seasonal)
    residual_values.push(point.residual)
  }
  let trend_scale = mad(trend_values)
  let seasonal_scale = mad(seasonal_values)
  let residual_scale = mad(residual_values)
  let total_scale = mad(data)
  let safe_total = if total_scale == 0.0 { 1.0 } else { total_scale }
  {
    count: data.length(),
    period: if period < 2 {
      2
    } else {
      period
    },
    trend_strength: transform_clip(trend_scale / safe_total, 0.0, 1.0),
    seasonal_strength: transform_clip(seasonal_scale / safe_total, 0.0, 1.0),
    residual_scale,
    forecastability: transform_clip(1.0 - residual_scale / safe_total, 0.0, 1.0),
  }
}

///|
pub fn timeseries_window_summaries(
  data : Array[Double],
  window : Int,
) -> Array[TimeSeriesWindow] {
  let result = []
  let width = if window < 2 { 2 } else { window }
  for index = 0; index < data.length(); index = index + 1 {
    let start = if index + 1 > width { index + 1 - width } else { 0 }
    let values = []
    for cursor = start; cursor <= index; cursor = cursor + 1 {
      values.push(data[cursor])
    }
    let slope = if values.length() < 2 {
      0.0
    } else {
      timeseries_regression_slope(values, identity_indices(values.length()))
    }
    result.push({
      start,
      end: index,
      center: median(values),
      scale: mad(values),
      slope,
    })
  }
  result
}

///|
fn identity_indices(length : Int) -> Array[Double] {
  let result = []
  for index = 0; index < length; index = index + 1 {
    result.push(index.to_double())
  }
  result
}

///|
pub fn timeseries_window_centers(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  let result = []
  for item in timeseries_window_summaries(data, window) {
    result.push(item.center)
  }
  result
}

///|
pub fn timeseries_window_scales(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  let result = []
  for item in timeseries_window_summaries(data, window) {
    result.push(item.scale)
  }
  result
}

///|
pub fn timeseries_window_slopes(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  let result = []
  for item in timeseries_window_summaries(data, window) {
    result.push(item.slope)
  }
  result
}

///|
pub fn timeseries_forecast_baseline(
  data : Array[Double],
  horizon : Int,
  period : Int,
  window : Int,
) -> Array[Double] {
  let summary = timeseries_summary(data, period, window)
  let result = []
  let count = if horizon < 0 { 0 } else { horizon }
  let safe_period = if period < 2 { 2 } else { period }
  let last = if data.length() == 0 { 0.0 } else { data[data.length() - 1] }
  for step = 0; step < count; step = step + 1 {
    let seasonal = if data.length() == 0 {
      0.0
    } else {
      timeseries_component_at(data, safe_period, data.length() + step)
    }
    result.push(last + seasonal * summary.seasonal_strength)
  }
  result
}

///|
pub fn timeseries_residuals(
  data : Array[Double],
  period : Int,
  window : Int,
) -> Array[Double] {
  let result = []
  for point in timeseries_points(data, period, window) {
    result.push(point.residual)
  }
  result
}

///|
pub fn timeseries_trend(
  data : Array[Double],
  period : Int,
  window : Int,
) -> Array[Double] {
  let result = []
  for point in timeseries_points(data, period, window) {
    result.push(point.trend)
  }
  result
}

///|
pub fn timeseries_seasonal(
  data : Array[Double],
  period : Int,
  window : Int,
) -> Array[Double] {
  let result = []
  for point in timeseries_points(data, period, window) {
    result.push(point.seasonal)
  }
  result
}

///|
pub fn timeseries_change_points(
  data : Array[Double],
  window : Int,
  threshold : Double,
) -> Array[Int] {
  signal_change_indices(signal_change_candidates(data, window, threshold))
}

///|
pub fn timeseries_period_scores(
  data : Array[Double],
  candidates : Array[Int],
) -> Array[Double] {
  let result = []
  for period in candidates {
    let safe_period = if period < 2 { 2 } else { period }
    result.push(abs_double(signal_autocorrelation(data, safe_period)))
  }
  result
}

///|
pub fn timeseries_best_period(
  data : Array[Double],
  candidates : Array[Int],
) -> Int {
  if candidates.length() == 0 {
    return 1
  }
  let scores = timeseries_period_scores(data, candidates)
  let mut best = 0
  for index = 1; index < scores.length(); index = index + 1 {
    if scores[index] > scores[best] {
      best = index
    }
  }
  candidates[best]
}

///|
pub fn timeseries_point_vector(point : TimeSeriesPoint) -> Array[Double] {
  [
    point.index.to_double(),
    point.value,
    point.trend,
    point.seasonal,
    point.residual,
  ]
}

///|
pub fn timeseries_summary_vector(summary : TimeSeriesSummary) -> Array[Double] {
  [
    summary.count.to_double(),
    summary.period.to_double(),
    summary.trend_strength,
    summary.seasonal_strength,
    summary.residual_scale,
    summary.forecastability,
  ]
}

///|
pub fn timeseries_summary_lines(summary : TimeSeriesSummary) -> Array[String] {
  [
    "count=" + summary.count.to_string(),
    "period=" + summary.period.to_string(),
    "trend_strength=" + summary.trend_strength.to_string(),
    "seasonal_strength=" + summary.seasonal_strength.to_string(),
    "residual_scale=" + summary.residual_scale.to_string(),
    "forecastability=" + summary.forecastability.to_string(),
  ]
}

///|
pub fn timeseries_summary_string(summary : TimeSeriesSummary) -> String {
  timeseries_summary_lines(summary).join("\n")
}

///|
pub fn timeseries_quality(
  data : Array[Double],
  period : Int,
  window : Int,
) -> Double {
  timeseries_summary(data, period, window).forecastability
}

///|
pub fn timeseries_reconstruct(
  data : Array[Double],
  period : Int,
  window : Int,
) -> Array[Double] {
  let result = []
  for point in timeseries_points(data, period, window) {
    result.push(point.trend + point.seasonal + point.residual)
  }
  result
}

///|
pub fn timeseries_reconstruction_error(
  data : Array[Double],
  period : Int,
  window : Int,
) -> Double {
  mean_absolute_error(data, timeseries_reconstruct(data, period, window))
}

///|
pub fn timeseries_summary_batch(
  data_sets : Array[Array[Double]],
  period : Int,
  window : Int,
) -> Array[Double] {
  let result = []
  for data in data_sets {
    result.push(timeseries_quality(data, period, window))
  }
  result
}