///|
/// Structured model and solve reports for CLI and CI artifacts.
///
/// Reports keep metrics close to the model that produced them, making it
/// possible to compare revisions without scraping human-oriented logs.
pub struct ModelReport {
  name : String
  variables : Int
  constraints : Int
  solved : Bool
  solutions : Int
  nodes : Int
  propagations : Int
  checks : Int
  pruned : Int
  signature : Int
}

///|
/// Build a report from a solver.
pub fn model_report(
  name : String,
  solver : Solver,
  solved : Bool,
  solutions : Int,
) -> ModelReport {
  let stats = solver.stats()
  {
    name,
    variables: solver.variable_count(),
    constraints: solver.constraint_count(),
    solved,
    solutions,
    nodes: stats.node_count(),
    propagations: stats.propagation_count(),
    checks: stats.check_count(),
    pruned: stats.pruned_count(),
    signature: solver.variable_count() * 31 +
    solver.constraint_count() * 37 +
    stats.node_count() * 41,
  }
}

///|
/// Return whether solved.
pub fn ModelReport::solved(self : ModelReport) -> Bool {
  self.solved
}

///|
/// Return work score.
pub fn ModelReport::work(self : ModelReport) -> Int {
  self.nodes + self.propagations + self.checks + self.pruned
}

///|
/// Return variable count.
pub fn ModelReport::variables(self : ModelReport) -> Int {
  self.variables
}

///|
/// Return constraint count.
pub fn ModelReport::constraints(self : ModelReport) -> Int {
  self.constraints
}

///|
/// Return solution count.
pub fn ModelReport::solutions(self : ModelReport) -> Int {
  self.solutions
}

///|
/// Return a stable one-line report.
pub fn ModelReport::describe(self : ModelReport) -> String {
  "\{self.name}: solved=\{self.solved}, solutions=\{self.solutions}, variables=\{self.variables}, constraints=\{self.constraints}, nodes=\{self.nodes}, propagations=\{self.propagations}, checks=\{self.checks}, pruned=\{self.pruned}, work=\{self.work()}"
}

///|
/// Render a report as CSV.
pub fn ModelReport::csv(self : ModelReport) -> String {
  "\{self.name},\{self.solved},\{self.solutions},\{self.variables},\{self.constraints},\{self.nodes},\{self.propagations},\{self.checks},\{self.pruned},\{self.work()}"
}

///|
/// Return a markdown table header.
pub fn model_report_header() -> String {
  "name | solved | solutions | variables | constraints | nodes | propagations | checks | pruned | work\n--- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---:"
}

///|
/// Render reports as a markdown table.
pub fn render_model_reports(reports : Array[ModelReport]) -> String {
  let builder = StringBuilder()
  builder.write_string(model_report_header())
  for report in reports {
    builder.write_char('\n')
    builder.write_string(
      "\{report.name} | \{report.solved} | \{report.solutions} | \{report.variables} | \{report.constraints} | \{report.nodes} | \{report.propagations} | \{report.checks} | \{report.pruned} | \{report.work()}",
    )
  }
  builder.to_string()
}

///|
/// Aggregate reports.
pub fn aggregate_model_reports(
  name : String,
  reports : Array[ModelReport],
) -> ModelReport {
  let mut variables = 0
  let mut constraints = 0
  let mut solutions = 0
  let mut nodes = 0
  let mut propagations = 0
  let mut checks = 0
  let mut pruned = 0
  let mut solved = true
  let mut signature = 17
  for report in reports {
    variables += report.variables
    constraints += report.constraints
    solutions += report.solutions
    nodes += report.nodes
    propagations += report.propagations
    checks += report.checks
    pruned += report.pruned
    solved = solved && report.solved
    signature = signature * 31 + report.signature
  }
  {
    name,
    variables,
    constraints,
    solved,
    solutions,
    nodes,
    propagations,
    checks,
    pruned,
    signature,
  }
}

///|
/// Return a regression ratio in integer percentage points.
pub fn report_work_change_percent(
  baseline : ModelReport,
  candidate : ModelReport,
) -> Int? {
  if baseline.work() == 0 {
    return None
  }
  Some((candidate.work() - baseline.work()) * 100 / baseline.work())
}

///|
/// Return whether the candidate is within a work tolerance.
pub fn report_within_tolerance(
  baseline : ModelReport,
  candidate : ModelReport,
  tolerance_percent : Int,
) -> Bool {
  !regressed(baseline.work(), candidate.work(), tolerance_percent)
}

///|
/// Return the most expensive report.
pub fn most_expensive_report(reports : Array[ModelReport]) -> ModelReport? {
  if reports.length() == 0 {
    return None
  }
  let mut result = reports[0]
  for report in reports {
    if report.work() > result.work() {
      result = report
    }
  }
  Some(result)
}

///|
/// Return solved report count.
pub fn solved_report_count(reports : Array[ModelReport]) -> Int {
  let mut result = 0
  for report in reports {
    if report.solved {
      result += 1
    }
  }
  result
}

///|
/// Return a report signature.
pub fn model_reports_signature(reports : Array[ModelReport]) -> Int {
  let mut result = 23
  for report in reports {
    result = result * 37 + report.signature
  }
  result
}