///|
pub struct Sample {
  timestamp : Float
  value : Float
} derive(Debug, Eq)

///|
pub fn sample(timestamp : Float, value : Float) -> Sample {
  { timestamp, value }
}

///|
pub fn Sample::is_finite(self : Sample) -> Bool {
  self.value == self.value && self.timestamp == self.timestamp
}

///|
pub struct TimeSeries {
  name : String
  samples : Array[Sample]
} derive(Debug, Eq)

///|
pub fn time_series(name : String, samples : Array[Sample]) -> TimeSeries {
  { name, samples }
}

///|
pub fn TimeSeries::length(self : TimeSeries) -> Int {
  self.samples.length()
}

///|
pub fn TimeSeries::at(self : TimeSeries, index : Int) -> Sample? {
  self.samples.get(index)
}

///|
pub fn TimeSeries::timestamps(self : TimeSeries) -> Array[Float] {
  self.samples.map(fn(item) { item.timestamp })
}

///|
pub fn TimeSeries::values(self : TimeSeries) -> Array[Float] {
  self.samples.map(fn(item) { item.value })
}

///|
pub fn TimeSeries::summary(self : TimeSeries) -> Statistics {
  summarize(self.values())
}

///|
pub fn TimeSeries::append(self : TimeSeries, point : Sample) -> TimeSeries {
  let samples = self.samples.copy()
  samples.push(point)
  { ..self, samples, }
}

///|
pub fn TimeSeries::valid(self : TimeSeries) -> TimeSeries {
  let samples : Array[Sample] = []
  for point in self.samples {
    if point.is_finite() {
      samples.push(point)
    }
  }
  { ..self, samples, }
}

///|
pub fn TimeSeries::between(
  self : TimeSeries,
  start : Float,
  stop : Float,
) -> TimeSeries {
  let samples : Array[Sample] = []
  for point in self.samples {
    if point.timestamp >= start && point.timestamp <= stop {
      samples.push(point)
    }
  }
  { ..self, samples, }
}

///|
pub fn TimeSeries::outliers(
  self : TimeSeries,
  threshold : Float,
) -> Array[Sample] {
  let result : Array[Sample] = []
  let stats = self.summary()
  let limit = threshold.abs() * stats.standard_deviation
  for point in self.samples {
    if stats.count > 1 && (point.value - stats.mean).abs() > limit {
      result.push(point)
    }
  }
  result
}

///|
pub fn TimeSeries::normal(
  self : TimeSeries,
  threshold : Float,
) -> Array[Sample] {
  let result : Array[Sample] = []
  let outliers = self.outliers(threshold)
  for point in self.samples {
    if !outliers.contains(point) {
      result.push(point)
    }
  }
  result
}

///|
pub fn TimeSeries::moving_average(
  self : TimeSeries,
  window : Int,
) -> TimeSeries {
  let averages = moving_average(self.values(), window)
  let samples : Array[Sample] = []
  for index, point in self.samples {
    samples.push({ timestamp: point.timestamp, value: averages[index] })
  }
  { ..self, samples, }
}

///|
pub fn TimeSeries::ewma(self : TimeSeries, alpha : Float) -> TimeSeries {
  let samples : Array[Sample] = []
  let safe_alpha = clamp(alpha, 0.0, 1.0)
  let mut previous : Float = 0.0
  for index, point in self.samples {
    if index == 0 {
      previous = point.value
    } else {
      previous = safe_alpha * point.value + (1.0 - safe_alpha) * previous
    }
    samples.push({ timestamp: point.timestamp, value: previous })
  }
  { ..self, samples, }
}

///|
pub fn TimeSeries::first_derivative(self : TimeSeries) -> TimeSeries {
  let samples : Array[Sample] = []
  for index, point in self.samples {
    if index == 0 {
      samples.push({ timestamp: point.timestamp, value: 0.0 })
    } else {
      let previous = self.samples[index - 1]
      let delta_time = point.timestamp - previous.timestamp
      let slope : Float = if delta_time == 0.0 {
        0.0
      } else {
        (point.value - previous.value) / delta_time
      }
      samples.push({ timestamp: point.timestamp, value: slope })
    }
  }
  { ..self, samples, }
}

///|
pub fn TimeSeries::trend(self : TimeSeries) -> Float {
  if self.samples.length() < 2 {
    0.0
  } else {
    let first = self.samples[0]
    let last = self.samples[self.samples.length() - 1]
    let delta = last.timestamp - first.timestamp
    if delta == 0.0 {
      0.0
    } else {
      (last.value - first.value) / delta
    }
  }
}

///|
pub fn TimeSeries::range(self : TimeSeries) -> Float {
  let stats = self.summary()
  stats.maximum - stats.minimum
}

///|
pub fn TimeSeries::integral(self : TimeSeries) -> Float {
  let mut total : Float = 0.0
  for index, point in self.samples {
    if index > 0 {
      let previous = self.samples[index - 1]
      total = total +
        (point.value + previous.value) /
        2.0 *
        (point.timestamp - previous.timestamp)
    }
  }
  total
}

///|
pub fn TimeSeries::time_weighted_mean(self : TimeSeries) -> Float {
  let duration : Float = if self.samples.length() < 2 {
    0.0
  } else {
    self.samples[self.samples.length() - 1].timestamp -
    self.samples[0].timestamp
  }
  if duration == 0.0 {
    0.0
  } else {
    self.integral() / duration
  }
}

///|
pub fn TimeSeries::interpolate(self : TimeSeries, timestamp : Float) -> Float? {
  if self.samples.length() == 0 {
    None
  } else if timestamp <= self.samples[0].timestamp {
    Some(self.samples[0].value)
  } else if timestamp >= self.samples[self.samples.length() - 1].timestamp {
    Some(self.samples[self.samples.length() - 1].value)
  } else {
    for index in 1.. TimeSeries {
  let samples : Array[Sample] = []
  for timestamp in timestamps {
    match self.interpolate(timestamp) {
      Some(value) => samples.push(sample(timestamp, value))
      None => ()
    }
  }
  { ..self, samples, }
}

///|
pub struct TrendSummary {
  slope : Float
  intercept : Float
  r_squared : Float
  direction : String
} derive(Debug, Eq)

///|
pub fn trend_summary(series : TimeSeries) -> TrendSummary {
  let n = series.samples.length()
  if n < 2 {
    { slope: 0.0, intercept: 0.0, r_squared: 0.0, direction: "flat" }
  } else {
    let x = series.timestamps()
    let y = series.values()
    let x_stats = summarize(x)
    let y_stats = summarize(y)
    let mut numerator : Float = 0.0
    let mut denominator : Float = 0.0
    for index, value in x {
      let dx = value - x_stats.mean
      numerator = numerator + dx * (y[index] - y_stats.mean)
      denominator = denominator + dx * dx
    }
    let slope : Float = if denominator == 0.0 {
      0.0
    } else {
      numerator / denominator
    }
    let intercept : Float = y_stats.mean - slope * x_stats.mean
    let direction = if slope > 0.000001 {
      "rising"
    } else if slope < -0.000001 {
      "falling"
    } else {
      "flat"
    }
    { slope, intercept, r_squared: correlation_squared(x, y), direction }
  }
}

///|
pub fn correlation_squared(left : Array[Float], right : Array[Float]) -> Float {
  if left.length() == 0 || left.length() != right.length() {
    0.0
  } else {
    let left_stats = summarize(left)
    let right_stats = summarize(right)
    let mut numerator : Float = 0.0
    let mut left_square : Float = 0.0
    let mut right_square : Float = 0.0
    for index, value in left {
      let left_delta = value - left_stats.mean
      let right_delta = right[index] - right_stats.mean
      numerator = numerator + left_delta * right_delta
      left_square = left_square + left_delta * left_delta
      right_square = right_square + right_delta * right_delta
    }
    if left_square == 0.0 || right_square == 0.0 {
      0.0
    } else {
      let correlation = numerator / (left_square * right_square).sqrt()
      correlation * correlation
    }
  }
}

///|
pub fn TimeSeries::trend_report(self : TimeSeries) -> ReportSection {
  let summary = trend_summary(self)
  {
    title: self.name,
    kind: Method,
    body: "direction=\{summary.direction}, slope=\{summary.slope}, r_squared=\{summary.r_squared}",
  }
}

///|
pub fn TimeSeries::to_table(self : TimeSeries) -> ReportTable {
  let rows : Array[Array[String]] = []
  for point in self.samples {
    rows.push(["\{point.timestamp}", "\{point.value}"])
  }
  table(["timestamp", self.name], rows)
}

///|
pub fn TimeSeries::to_markdown(self : TimeSeries) -> String {
  self.to_table().to_markdown()
}

///|
pub fn TimeSeries::quality_gate(
  self : TimeSeries,
  minimum_count : Int,
  maximum_outliers : Int,
  threshold : Float,
) -> Bool {
  self.valid().length() >= minimum_count &&
  self.outliers(threshold).length() <= maximum_outliers
}