///|
pub(all) struct PairDiagnostic {
  origin : Int32
  age : Int32
  base : Double
  observed : Double
  expected : Double
  residual : Double
  ratio : Double?
} derive(Debug)

///|
pub extend PairDiagnostic with @debug.Debug::{to_repr}

///|
/// Raw residuals are descriptive, not Mack standard errors.
pub fn pair_diagnostics(
  triangle : Triangle,
  pattern : Pattern,
) -> Array[PairDiagnostic] raise RunoffError {
  compatible(triangle, pattern)
  let out : Array[PairDiagnostic] = []
  for age in 0..<(triangle.order() - 1) {
    for origin in 0..<(triangle.order() - age - 1) {
      let base = triangle.rows[origin][age]
      let observed = triangle.rows[origin][age + 1]
      let expected = checked(
        base * pattern.factors[age],
        "diagnostic expected overflow",
      )
      let ratio = if base == 0.0 {
        None
      } else {
        Some(checked(observed / base, "diagnostic ratio overflow"))
      }
      out.push({
        origin,
        age,
        base,
        observed,
        expected,
        residual: observed - expected,
        ratio,
      })
    }
  }
  out
}

///|
pub(all) struct FitSummary {
  pairs : Int32
  zero_bases : Int32
  mean_absolute_residual : Double
  root_mean_square_residual : Double
  maximum_absolute_residual : Double
} derive(Debug)

///|
pub extend FitSummary with @debug.Debug::{to_repr}

///|
pub fn fit_summary(
  triangle : Triangle,
  pattern : Pattern,
) -> FitSummary raise RunoffError {
  let pairs = pair_diagnostics(triangle, pattern)
  let mut zero_bases = 0
  let mut absolute = 0.0
  let mut square = 0.0
  let mut maximum = 0.0
  for row in pairs {
    if row.base == 0.0 {
      zero_bases += 1
    }
    let a = row.residual.abs()
    absolute = checked(absolute + a, "absolute residual sum overflow")
    square = checked(
      square + checked(a * a, "squared residual overflow"),
      "residual square sum overflow",
    )
    if a > maximum {
      maximum = a
    }
  }
  let count = pairs.length().to_double()
  {
    pairs: pairs.length(),
    zero_bases,
    mean_absolute_residual: absolute / count,
    root_mean_square_residual: (square / count).sqrt(),
    maximum_absolute_residual: maximum,
  }
}