///|
pub(all) struct FeatureImpact {
  feature : String
  transitions : Int
  outcome_flips : Int
  flips_to_true : Int
  flips_to_false : Int
  unknown_transitions : Int
} derive(Debug, Eq)

///|
pub(all) struct ImpactReport {
  policy_name : String
  impacts : Array[FeatureImpact]
  source_rows : Int
  truncated : Bool
} derive(Debug, Eq)

///|
/// Measure how often changing one feature changes the outcome across a bounded
/// decision table. Counts are directional and therefore suitable for ranking,
/// not probability estimates.
pub fn Policy::impact_report(
  self : Policy,
  table_limit? : Int = 256,
) -> Result[ImpactReport, Diagnostic] {
  let table = match self.decision_table(limit=table_limit) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let impacts : Array[FeatureImpact] = []
  for feature in self.features {
    if feature.locked {
      continue
    }
    let mut transitions = 0
    let mut outcome_flips = 0
    let mut flips_to_true = 0
    let mut flips_to_false = 0
    let mut unknown_transitions = 0
    for row in table.rows {
      let current = environment_get(row.environment, feature.name)
      for candidate in feature.domain {
        if current == Some(candidate) {
          continue
        }
        transitions = transitions + 1
        let changed = environment_set(row.environment, feature.name, candidate)
        let next = evaluate(self.goal, changed).truth
        if next == Unknown || row.outcome == Unknown {
          unknown_transitions = unknown_transitions + 1
        }
        if next != row.outcome && next != Unknown && row.outcome != Unknown {
          outcome_flips = outcome_flips + 1
          if next == Yes {
            flips_to_true = flips_to_true + 1
          } else {
            flips_to_false = flips_to_false + 1
          }
        }
      }
    }
    impacts.push({
      feature: feature.name,
      transitions,
      outcome_flips,
      flips_to_true,
      flips_to_false,
      unknown_transitions,
    })
  }
  impacts.sort_by((left, right) => {
    let by_flips = right.outcome_flips.compare(left.outcome_flips)
    if by_flips != 0 {
      by_flips
    } else {
      left.feature.compare(right.feature)
    }
  })
  Ok({
    policy_name: self.name,
    impacts,
    source_rows: table.rows.length(),
    truncated: table.truncated,
  })
}

///|
pub fn ImpactReport::to_markdown(self : ImpactReport) -> String {
  let out = StringBuilder::new()
  out.write_string("# Feature impact: \{self.policy_name}\n\n")
  out.write_string(
    "| feature | transitions | flips | to true | to false | unknown |\n",
  )
  out.write_string("| --- | ---: | ---: | ---: | ---: | ---: |\n")
  for item in self.impacts {
    out.write_string(
      "| \{item.feature} | \{item.transitions} | \{item.outcome_flips} | \{item.flips_to_true} | \{item.flips_to_false} | \{item.unknown_transitions} |\n",
    )
  }
  if self.truncated {
    out.write_string("\n_Report is based on a truncated decision table._\n")
  }
  out.to_string()
}