///|
/// A lightweight seasonal profile updated online with exponential forgetting.
pub struct SeasonalProfile {
  period : Int
  levels : Array[Double]
  counts : Array[Int]
  alpha : Double
  mut index : Int
}

///|
pub fn SeasonalProfile::new(
  period : Int,
  alpha? : Double = 0.1,
) -> SeasonalProfile {
  let safe_period = if period < 1 { 1 } else { period }
  {
    period: safe_period,
    levels: Array::make(safe_period, 0.0),
    counts: Array::make(safe_period, 0),
    alpha: clamp_probability(alpha),
    index: 0,
  }
}

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

///|
pub fn SeasonalProfile::update(
  self : SeasonalProfile,
  value : Double,
) -> Double {
  if !is_finite(value) {
    return 0.0
  }
  let bucket = self.index % self.period
  self.index += 1
  let baseline = self.levels[bucket]
  if self.counts[bucket] == 0 {
    self.levels[bucket] = value
  } else {
    self.levels[bucket] = baseline + self.alpha * (value - baseline)
  }
  self.counts[bucket] += 1
  value - baseline
}

///|
pub fn SeasonalProfile::baseline(self : SeasonalProfile, index : Int) -> Double {
  if self.period == 0 {
    0.0
  } else {
    self.levels[(index % self.period + self.period) % self.period]
  }
}

///|
pub fn SeasonalProfile::levels(self : SeasonalProfile) -> Array[Double] {
  let result : Array[Double] = []
  for level in self.levels {
    result.push(level)
  }
  result
}

///|
pub fn SeasonalProfile::count(self : SeasonalProfile, index : Int) -> Int {
  if self.period == 0 {
    0
  } else {
    self.counts[(index % self.period + self.period) % self.period]
  }
}

///|
pub fn deseasonalize(values : Array[Double], period : Int) -> Array[Double] {
  let profile = SeasonalProfile::new(period, alpha=1.0)
  for value in values {
    ignore(profile.update(value))
  }
  let result : Array[Double] = []
  for i, value in values {
    result.push(value - profile.baseline(i))
  }
  result
}

///|
pub fn seasonal_naive_forecast(
  values : Array[Double],
  period : Int,
  horizon : Int,
) -> Array[Double] {
  let result : Array[Double] = []
  let safe_period = if period < 1 { 1 } else { period }
  if values.length() == 0 {
    return result
  }
  for i in 0..= 0 && source < values.length() {
        values[source]
      } else {
        values[values.length() - 1]
      },
    )
  }
  result
}

///|
pub fn seasonal_strength(values : Array[Double], period : Int) -> Double {
  if values.length() < period * 2 {
    return 0.0
  }
  let residuals = deseasonalize(values, period)
  let total_variance = variance(values)
  if total_variance <= 1.0e-12 {
    0.0
  } else {
    clamp_probability(1.0 - variance(residuals) / total_variance)
  }
}

///|
pub struct SeasonalAnomalyDetector {
  profile : SeasonalProfile
  threshold : Double
  mut index : Int
}

///|
pub fn SeasonalAnomalyDetector::new(
  period : Int,
  threshold? : Double = 3.0,
  alpha? : Double = 0.1,
) -> SeasonalAnomalyDetector {
  {
    profile: SeasonalProfile::new(period, alpha~),
    threshold: if threshold <= 0.0 {
      3.0
    } else {
      threshold
    },
    index: 0,
  }
}

///|
pub fn SeasonalAnomalyDetector::update(
  self : SeasonalAnomalyDetector,
  value : Double,
) -> DetectionResult {
  self.index += 1
  if !is_finite(value) {
    return DetectionResult::quiet(index=self.index)
  }
  let residual = self.profile.update(value)
  if self.profile.count(self.index - 1) < 2 {
    return DetectionResult::quiet(index=self.index)
  }
  let scale = 1.0
  let z = absolute(residual) / scale
  DetectionResult::new(
    z >= self.threshold,
    z / self.threshold,
    clamp_probability(z / (z + 1.0)),
    direction_for_delta(residual),
    self.index,
    evidence=residual,
  )
}