///|
pub struct SourceSensitivity {
  omitted : String
  conflict_without : Double
  conflict_change : Double
  pignistic_variation : Double
  winner_without : String
  winner_changed : Bool
} derive(Debug, Eq)

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

///|
pub extend SourceSensitivity with Eq::{equal, not_equal}

///|
fn maximum_index(values : Array[Double]) -> Int32 {
  let mut best = 0
  for i = 1; i < values.length(); i = i + 1 {
    if values[i] > values[best] {
      best = i
    }
  }
  best
}

///|
/// Leave-one-out is a sensitivity diagnostic, not source-reliability learning.
/// Limited to 2..16 sources. The pair budget is shared across baseline and
/// all reruns; no per-source failures are hidden. Total conflict raises an error.
pub fn leave_one_out(
  sources : Array[Source],
  rule : Rule,
  budget~ : Limits = default_limits(),
) -> Array[SourceSensitivity] raise BeliefError {
  if sources.length() < 2 || sources.length() > 16 {
    raise Invalid("leave-one-out requires 2..16 sources")
  }
  let baseline = pipeline(sources, rule, budget~)
  let probabilities = baseline.mass().pignistic()
  let winner = maximum_index(probabilities)
  let labels = baseline.mass().frame().labels()
  let result : Array[SourceSensitivity] = []
  let mut work = baseline.pair_work()
  for omitted = 0; omitted < sources.length(); omitted = omitted + 1 {
    let remaining : Array[Source] = []
    for i = 0; i < sources.length(); i = i + 1 {
      if i != omitted {
        remaining.push(sources[i])
      }
    }
    if work >= budget.pairs {
      raise Budget("sensitivity aggregate pair budget exhausted")
    }
    let local_budget = limits(
      max_focals=budget.focals,
      max_pairs=budget.pairs - work,
      max_dense_cells=budget.dense,
    )
    let rerun = pipeline(remaining, rule, budget=local_budget)
    work = work + rerun.pair_work()
    let alternate = rerun.mass().pignistic()
    let terms : Array[Double] = []
    for i = 0; i < probabilities.length(); i = i + 1 {
      terms.push(magnitude(probabilities[i] - alternate[i]))
    }
    let alternate_winner = maximum_index(alternate)
    result.push({
      omitted: sources[omitted].name,
      conflict_without: rerun.mass().conflict(),
      conflict_change: rerun.mass().conflict() - baseline.mass().conflict(),
      pignistic_variation: stable_sum(terms) * 0.5,
      winner_without: labels[alternate_winner],
      winner_changed: alternate_winner != winner,
    })
  }
  result
}