///|
/// Effect and prediction diagnostics for one subgroup.
pub struct FairSubgroupEffect {
  group : Int
  count : Int
  treated_count : Int
  control_count : Int
  effect : Double
  standard_error : Double
  positive_rate : Double
}

///|
/// Fairness report across subgroups.
pub struct FairnessReport {
  subgroups : Array[FairSubgroupEffect]
  maximum_effect_gap : Double
  maximum_positive_rate_gap : Double
  equal_opportunity_gap : Double
  passes : Bool
}

///|
/// Computes subgroup treatment effects and positive prediction rates.
pub fn subgroup_effects(
  groups : Array[Int],
  treatment : Array[Bool],
  outcomes : Array[Double],
  predictions : Array[Bool],
) -> Array[FairSubgroupEffect] {
  let n = groups
    .length()
    .min(treatment.length())
    .min(outcomes.length())
    .min(predictions.length())
  let unique : Array[Int] = Array::new()
  for group in groups[:n] {
    if !unique.contains(group) {
      unique.push(group)
    }
  }
  let result : Array[FairSubgroupEffect] = Array::new(capacity=unique.length())
  for group in unique {
    let treated = Array::new()
    let control = Array::new()
    let mut positive = 0
    for i in 0.. Array[Array[Double]] {
  let n = groups.length().min(actual.length()).min(predicted.length())
  let unique : Array[Int] = Array::new()
  for group in groups[:n] {
    if !unique.contains(group) {
      unique.push(group)
    }
  }
  let result : Array[Array[Double]] = Array::new(capacity=unique.length())
  for group in unique {
    let mut positives = 0
    let mut true_positives = 0
    for i in 0.. Array[Array[Double]] {
  let n = groups.length().min(actual.length()).min(predicted.length())
  let unique : Array[Int] = Array::new()
  for group in groups[:n] {
    if !unique.contains(group) {
      unique.push(group)
    }
  }
  let result : Array[Array[Double]] = Array::new(capacity=unique.length())
  for group in unique {
    let mut negatives = 0
    let mut false_positives = 0
    for i in 0.. FairnessReport {
  let effects = subgroup_effects(groups, treatment, outcomes, predictions)
  let mut maximum_effect = 0.0
  let mut maximum_rate = 0.0
  for first in effects {
    for second in effects {
      let effect_gap = (first.effect - second.effect).abs()
      let rate_gap = (first.positive_rate - second.positive_rate).abs()
      if effect_gap > maximum_effect {
        maximum_effect = effect_gap
      }
      if rate_gap > maximum_rate {
        maximum_rate = rate_gap
      }
    }
  }
  let tpr = fair_true_positive_rates(groups, treatment, predictions)
  let mut opportunity_gap = 0.0
  for first in tpr {
    for second in tpr {
      let gap = (first[1] - second[1]).abs()
      if gap > opportunity_gap {
        opportunity_gap = gap
      }
    }
  }
  {
    subgroups: effects,
    maximum_effect_gap: maximum_effect,
    maximum_positive_rate_gap: maximum_rate,
    equal_opportunity_gap: opportunity_gap,
    passes: maximum_effect <= maximum_effect_gap &&
    maximum_rate <= maximum_rate_gap,
  }
}

///|
/// Computes demographic parity ratio from subgroup positive rates.
pub fn demographic_parity_ratio(report : FairnessReport) -> Double {
  if report.subgroups.length() == 0 {
    return 1.0
  }
  let mut minimum = report.subgroups[0].positive_rate
  let mut maximum = minimum
  for subgroup in report.subgroups {
    if subgroup.positive_rate < minimum {
      minimum = subgroup.positive_rate
    }
    if subgroup.positive_rate > maximum {
      maximum = subgroup.positive_rate
    }
  }
  if maximum == 0.0 {
    1.0
  } else {
    minimum / maximum
  }
}

///|
/// Returns a compact fairness summary vector.
pub fn fairness_summary(report : FairnessReport) -> Array[Double] {
  [
    report.subgroups.length().to_double(),
    report.maximum_effect_gap,
    report.maximum_positive_rate_gap,
    report.equal_opportunity_gap,
    demographic_parity_ratio(report),
    if report.passes {
      1.0
    } else {
      0.0
    },
  ]
}