///|
/// Leaf-level causes that currently prevent a trace from being true. This is
/// intentionally structural: callers receive the exact evaluated predicates,
/// not a second heuristic explanation of the policy.
pub fn Trace::blocking_leaves(self : Trace) -> Array[Trace] {
  let result : Array[Trace] = []
  collect_blocking_leaves(self, result)
  result
}

///|
fn collect_blocking_leaves(trace : Trace, result : Array[Trace]) -> Unit {
  if trace.children.is_empty() {
    if trace.truth != Yes {
      result.push(trace)
    }
    return
  }
  match trace.truth {
    Yes => ()
    No | Unknown => {
      let before = result.length()
      for child in trace.children {
        if child.truth != Yes {
          collect_blocking_leaves(child, result)
        }
      }
      if result.length() == before {
        result.push(trace)
      }
    }
  }
}

///|
pub(all) struct DecisionRow {
  environment : Environment
  outcome : Truth
} derive(Debug, Eq)

///|
pub(all) struct DecisionTable {
  policy_name : String
  rows : Array[DecisionRow]
  combinations : Int
  truncated : Bool
} derive(Debug, Eq)

///|
priv struct TableState {
  model : Policy
  limit : Int
  rows : Array[DecisionRow]
  mut combinations : Int
  mut truncated : Bool
}

///|
/// Enumerate the feasible decision surface. Mutable feature domains are
/// expanded; locked features stay at their current value. `limit` bounds the
/// materialized rows so the API is safe for interactive tools.
pub fn Policy::decision_table(
  self : Policy,
  limit? : Int = 256,
) -> Result[DecisionTable, Diagnostic] {
  if limit <= 0 {
    return Err({
      code: "E_TABLE_LIMIT",
      message: "decision table limit must be positive",
      span: { offset: 0, line: 1, column: 1 },
    })
  }
  let validation = self.validate()
  if !validation.is_empty() {
    return Err(validation[0])
  }
  let state = TableState::{
    model: self,
    limit,
    rows: [],
    combinations: 0,
    truncated: false,
  }
  enumerate_table(state, 0, self.environment())
  Ok({
    policy_name: self.name,
    rows: state.rows,
    combinations: state.combinations,
    truncated: state.truncated,
  })
}

///|
fn enumerate_table(
  state : TableState,
  index : Int,
  environment : Environment,
) -> Unit {
  if index >= state.model.features.length() {
    state.combinations = state.combinations + 1
    if state.rows.length() < state.limit {
      state.rows.push({
        environment,
        outcome: evaluate(state.model.goal, environment).truth,
      })
    } else {
      state.truncated = true
    }
    return
  }
  let item = state.model.features[index]
  if item.locked {
    enumerate_table(state, index + 1, environment)
    return
  }
  for candidate in item.domain {
    enumerate_table(
      state,
      index + 1,
      environment_set(environment, item.name, candidate),
    )
  }
}

///|
pub fn DecisionTable::count(self : DecisionTable, truth : Truth) -> Int {
  self.rows.count_if(row => row.outcome == truth)
}

///|
pub fn DecisionTable::to_markdown(self : DecisionTable) -> String {
  let out = StringBuilder::new()
  out.write_string("# Decision table: \{self.policy_name}\n\n")
  if self.rows.is_empty() {
    out.write_string("No rows.\n")
    return out.to_string()
  }
  for binding in self.rows[0].environment {
    out.write_string("| \{binding.name} ")
  }
  out.write_string("| outcome |\n")
  for _ in self.rows[0].environment {
    out.write_string("| --- ")
  }
  out.write_string("| --- |\n")
  for row in self.rows {
    for binding in row.environment {
      out.write_string("| \{binding.value.render()} ")
    }
    out.write_string("| \{row.outcome.render()} |\n")
  }
  if self.truncated {
    out.write_string("\n_Table truncated after \{self.rows.length()} rows._\n")
  }
  out.to_string()
}

///|
/// Serialize a programmatically assembled policy back to canonical FlipSpec.
/// The result can be parsed again, which makes it useful for editors and model
/// migration tools.
pub fn Policy::to_source(self : Policy) -> String {
  let out = StringBuilder::new()
  out.write_string("spec \{self.name}\n")
  for item in self.features {
    out.write_string("feature \{item.name} = \{item.current.render()} in [")
    for index, value in item.domain {
      if index > 0 {
        out.write_string(", ")
      }
      out.write_string(value.render())
    }
    out.write_string("] cost \{item.cost}")
    if item.locked {
      out.write_string(" locked")
    }
    out.write_char('\n')
  }
  out.write_string("goal \{self.goal.render()}\n")
  out.to_string()
}