///|
/// A compact convergence summary for CLI and benchmark output.
pub struct SearchSummary {
  instance_name : String
  iterations : Int
  seed_objective : Double
  best_objective : Double
  absolute_improvement : Double
  relative_improvement : Double
  accepted_moves : Int
  improved_moves : Int
  local_improvements : Int
  evaluated_candidates : Int
  feasible : Bool
} derive(Debug, ToJson)

///|
pub fn SearchSummary::instance_name(self : SearchSummary) -> String {
  self.instance_name
}

///|
pub fn SearchSummary::iterations(self : SearchSummary) -> Int {
  self.iterations
}

///|
pub fn SearchSummary::seed_objective(self : SearchSummary) -> Double {
  self.seed_objective
}

///|
pub fn SearchSummary::best_objective(self : SearchSummary) -> Double {
  self.best_objective
}

///|
pub fn SearchSummary::absolute_improvement(self : SearchSummary) -> Double {
  self.absolute_improvement
}

///|
pub fn SearchSummary::relative_improvement(self : SearchSummary) -> Double {
  self.relative_improvement
}

///|
pub fn SearchSummary::accepted_moves(self : SearchSummary) -> Int {
  self.accepted_moves
}

///|
pub fn SearchSummary::improved_moves(self : SearchSummary) -> Int {
  self.improved_moves
}

///|
pub fn SearchSummary::local_improvements(self : SearchSummary) -> Int {
  self.local_improvements
}

///|
pub fn SearchSummary::evaluated_candidates(self : SearchSummary) -> Int {
  self.evaluated_candidates
}

///|
pub fn SearchSummary::is_feasible(self : SearchSummary) -> Bool {
  self.feasible
}

///|
/// Converts a solver result into stable, machine-readable metrics.
pub fn summarize_search(result : @core.SolveResult) -> SearchSummary {
  let seed = result.stats.seed_objective
  let best = result.stats.best_objective
  let absolute = seed - best
  let relative = if seed == 0.0 { 0.0 } else { absolute / seed }
  {
    instance_name: result.instance.instance_name(),
    iterations: result.stats.iterations,
    seed_objective: seed,
    best_objective: best,
    absolute_improvement: absolute,
    relative_improvement: relative,
    accepted_moves: result.stats.accepted_moves(),
    improved_moves: result.stats.improved_moves(),
    local_improvements: result.stats.local_improvements(),
    evaluated_candidates: result.stats.evaluated_candidates(),
    feasible: result.best_solution.is_feasible(),
  }
}

///|
pub fn search_summary_text(summary : SearchSummary) -> String {
  [
    "Instance: \{summary.instance_name}",
    "Iterations: \{summary.iterations}",
    "Seed objective: \{summary.seed_objective}",
    "Best objective: \{summary.best_objective}",
    "Absolute improvement: \{summary.absolute_improvement}",
    "Relative improvement: \{summary.relative_improvement}",
    "Accepted moves: \{summary.accepted_moves}",
    "Improved moves: \{summary.improved_moves}",
    "Local improvements: \{summary.local_improvements}",
    "Evaluated candidates: \{summary.evaluated_candidates}",
    "Feasible: \{summary.feasible}",
  ].join("\n")
}

///|
/// Chooses the best result by objective, preferring feasible solutions on ties.
pub fn best_result_index(results : Array[@core.SolveResult]) -> Int? {
  if results.is_empty() {
    return None
  }
  let mut selected = 0
  for index in 1.. Bool {
  let first = solve(instance, config~)
  let second = solve(instance, config~)
  @core.solution_signature(instance, first.best_solution) ==
  @core.solution_signature(instance, second.best_solution)
}

///|
/// Returns actionable quality flags without changing solver behavior.
pub fn quality_flags(
  instance : @core.VrpInstance,
  result : @core.SolveResult,
) -> Array[String] {
  let flags : Array[String] = []
  if !result.best_solution.feasible {
    flags.push("best solution is infeasible")
  }
  if result.best_solution.unassigned_customers().length() > 0 {
    flags.push("some customers are unassigned")
  }
  let validation = @core.validate_solution(instance, result.best_solution)
  if !validation.is_valid() {
    flags.append(@core.validation_messages(validation))
  }
  if result.stats.accepted_moves() == 0 && result.stats.iterations > 0 {
    flags.push("no candidate move was accepted")
  }
  flags
}