///|
/// A contiguous flat or saturated portion of a waveform.
pub struct SampleSegment {
  start_index : Int
  end_index : Int
  duration : Double
}

///|
/// Machine-readable quality findings for a trace.
pub enum QualityFlag {
  EmptyTrace
  InvalidSampleRate
  TooShort
  Flatline
  Clipped
  BaselineDrift
  LowSignalToNoise
  ExcessiveGaps
}

///|
/// Summary of deterministic waveform quality checks.
pub struct QualityReport {
  score : Double
  flags : Array[QualityFlag]
  sample_count : Int
  peak_amplitude : Double
  rms_amplitude : Double
  noise_rms : Double
  signal_to_noise : Double
  clipped_fraction : Double
  baseline_slope : Double
  flat_fraction : Double
}

///|
/// Return a stable finite check for the values representable in portable
/// MoonBit arithmetic. Infinity and NaN are rejected by the self-inequality.
fn is_valid_double(value : Double) -> Bool {
  value == value && value.abs() < 1.0e300
}

///|
/// Count invalid floating-point samples.
pub fn invalid_sample_count(data : Array[Double]) -> Int {
  let mut count = 0
  for value in data {
    if !is_valid_double(value) {
      count += 1
    }
  }
  count
}

///|
/// Detect samples at or beyond a symmetric amplitude limit.
pub fn clipped_fraction(data : Array[Double], limit : Double) -> Double {
  if data.is_empty() || limit <= 0.0 {
    return 0.0
  }
  let mut clipped = 0
  for value in data {
    if value.abs() >= limit {
      clipped += 1
    }
  }
  clipped.to_double() / data.length().to_double()
}

///|
/// Find contiguous runs with absolute amplitude no greater than a tolerance.
pub fn flat_segments(
  data : Array[Double],
  tolerance : Double,
  sample_rate : Double,
) -> Array[SampleSegment] {
  let result = Array::new()
  if data.is_empty() {
    return result
  }
  let threshold = if tolerance < 0.0 { 0.0 } else { tolerance }
  let mut active = false
  let mut start = 0
  for index = 0; index < data.length(); index = index + 1 {
    let flat = data[index].abs() <= threshold
    if flat && !active {
      active = true
      start = index
    } else if !flat && active {
      let end = index - 1
      result.push({
        start_index: start,
        end_index: end,
        duration: if sample_rate > 0.0 {
          (end - start + 1).to_double() / sample_rate
        } else {
          0.0
        },
      })
      active = false
    }
  }
  if active {
    let end = data.length() - 1
    result.push({
      start_index: start,
      end_index: end,
      duration: if sample_rate > 0.0 {
        (end - start + 1).to_double() / sample_rate
      } else {
        0.0
      },
    })
  }
  result
}

///|
/// Detect saturated runs using a symmetric clipping threshold.
pub fn clipping_segments(
  data : Array[Double],
  limit : Double,
  sample_rate : Double,
) -> Array[SampleSegment] {
  let result = Array::new()
  if data.is_empty() || limit <= 0.0 {
    return result
  }
  let mut active = false
  let mut start = 0
  for index = 0; index < data.length(); index = index + 1 {
    let clipped = data[index].abs() >= limit
    if clipped && !active {
      active = true
      start = index
    } else if !clipped && active {
      let end = index - 1
      result.push({
        start_index: start,
        end_index: end,
        duration: if sample_rate > 0.0 {
          (end - start + 1).to_double() / sample_rate
        } else {
          0.0
        },
      })
      active = false
    }
  }
  if active {
    let end = data.length() - 1
    result.push({
      start_index: start,
      end_index: end,
      duration: if sample_rate > 0.0 {
        (end - start + 1).to_double() / sample_rate
      } else {
        0.0
      },
    })
  }
  result
}

///|
/// Estimate a baseline slope in amplitude units per sample.
pub fn baseline_slope(data : Array[Double]) -> Double {
  let n = data.length()
  if n <= 1 {
    return 0.0
  }
  let mean_x = (n - 1).to_double() / 2.0
  let mean_y = mean_samples(data)
  let mut covariance = 0.0
  let mut variance = 0.0
  for index = 0; index < n; index = index + 1 {
    let dx = index.to_double() - mean_x
    covariance += dx * (data[index] - mean_y)
    variance += dx * dx
  }
  if variance == 0.0 {
    0.0
  } else {
    covariance / variance
  }
}

///|
/// Estimate noise RMS from first differences.
pub fn difference_noise_rms(data : Array[Double]) -> Double {
  if data.length() < 2 {
    return 0.0
  }
  let differences = Array::new(capacity=data.length() - 1)
  for index = 1; index < data.length(); index = index + 1 {
    differences.push(data[index] - data[index - 1])
  }
  standard_deviation(differences) / 2.0.sqrt()
}

///|
/// Estimate signal-to-noise ratio in decibels using difference noise.
pub fn signal_to_noise_db(data : Array[Double]) -> Double {
  let signal = rms_samples(data)
  let noise = difference_noise_rms(data)
  if signal <= 0.0 || noise <= 0.0 {
    0.0
  } else {
    20.0 * @math.log10(signal / noise)
  }
}

///|
/// Replace isolated spikes with the local median.
pub fn repair_spikes(
  data : Array[Double],
  window_size : Int,
  threshold : Double,
) -> Array[Double] {
  let result = data.copy()
  let median = median_filter(data, window_size)
  let limit = if threshold < 0.0 { 0.0 } else { threshold }
  for index = 0; index < result.length(); index = index + 1 {
    if (result[index] - median[index]).abs() > limit {
      result[index] = median[index]
    }
  }
  result
}

///|
/// Linearly interpolate samples in a short clipped run.
pub fn repair_clipped_samples(
  data : Array[Double],
  limit : Double,
) -> Array[Double] {
  let result = data.copy()
  if limit <= 0.0 {
    return result
  }
  for index = 0; index < result.length(); index = index + 1 {
    if result[index].abs() >= limit {
      let left = if index == 0 { result[index] } else { result[index - 1] }
      let right = if index + 1 >= result.length() {
        result[index]
      } else {
        result[index + 1]
      }
      result[index] = 0.5 * (left + right)
    }
  }
  result
}

///|
/// Build a quality report using conservative, documented defaults.
pub fn assess_quality(
  trace : Trace,
  clip_limit : Double,
  flat_tolerance : Double,
) -> QualityReport {
  let data = trace.data
  let count = data.length()
  let peak = maximum_sample(data).abs()
  let rms = rms_samples(data)
  let noise = difference_noise_rms(data)
  let snr = signal_to_noise_db(data)
  let clipped = clipped_fraction(data, clip_limit)
  let flats = flat_segments(data, flat_tolerance, trace.sampling_rate)
  let flat_samples = flats.fold(init=0, (total, segment) => {
    total + segment.end_index - segment.start_index + 1
  })
  let flat_ratio = if count == 0 {
    0.0
  } else {
    flat_samples.to_double() / count.to_double()
  }
  let slope = baseline_slope(data)
  let flags = Array::new()
  if count == 0 {
    flags.push(EmptyTrace)
  }
  if trace.sampling_rate <= 0.0 {
    flags.push(InvalidSampleRate)
  }
  if count > 0 && count < 16 {
    flags.push(TooShort)
  }
  if flat_ratio >= 0.5 && count > 0 {
    flags.push(Flatline)
  }
  if clipped >= 0.01 {
    flags.push(Clipped)
  }
  if slope.abs() > 0.01 * (peak + 1.0) && count > 2 {
    flags.push(BaselineDrift)
  }
  if snr < 3.0 && count > 1 {
    flags.push(LowSignalToNoise)
  }
  if invalid_sample_count(data) > 0 {
    flags.push(ExcessiveGaps)
  }
  let penalty = flags.length().to_double() * 0.15
  let score = if 1.0 - penalty < 0.0 { 0.0 } else { 1.0 - penalty }
  {
    score,
    flags,
    sample_count: count,
    peak_amplitude: peak,
    rms_amplitude: rms,
    noise_rms: noise,
    signal_to_noise: snr,
    clipped_fraction: clipped,
    baseline_slope: slope,
    flat_fraction: flat_ratio,
  }
}

///|
/// Validate metadata and sample values before entering a processing pipeline.
pub fn validate_trace(trace : Trace) -> Result[Unit, String] {
  if trace.sampling_rate <= 0.0 {
    return Err("Trace sampling rate must be positive")
  }
  if invalid_sample_count(trace.data) > 0 {
    return Err("Trace contains an invalid floating-point sample")
  }
  Ok(())
}

///|
/// Return true when no quality flag is present.
pub fn is_good_quality(report : QualityReport) -> Bool {
  report.flags.is_empty()
}