///|
/// Runnable example: what a plan someone already has is worth, measured against a bound.
///
/// This is the workflow the package exists for in production settings — an outer heuristic
/// (or a human) produces a plan, and the question is not "is there a better plan" but "how far
/// from optimal could this one be". A bound answers that, and it is a *relaxation* that
/// produces it:
///
///   min 6a + 5b
///   s.t. 4a + 2b >= 5      (demand: pattern A covers 4 units, pattern B covers 2)
///        a, b >= 0 integers
///
/// Pattern A costs 6 per shift and covers 4 units, pattern B costs 5 and covers 2. The
/// relaxation (the same model without integrality) reaches 1.25 shifts of A, which no roster
/// can: 6 * 1.25 = 7.5 is a valid *bound* on any whole plan, and the best whole plan costs 11
/// at (1, 1). A plan of (2, 0) costs 12, so it is at most 4.5 — 60% of the bound — above the
/// best possible, while the bound itself sits 31.8% below the true optimum. Both numbers are
/// printed, because a bound that is reported without its own looseness is a number a reader
/// will over-trust.
///
/// Run with: `moon run examples/schedule_bound`
fn main {
  let model = @model.Model::new(@model.Sense::Minimize)
  let a = model.add_var("a", lb=0.0, is_int=true)
  let b = model.add_var("b", lb=0.0, is_int=true)
  model.set_objective([(a, 6.0), (b, 5.0)])
  model.add_named_constraint(
    [(a, 4.0), (b, 2.0)],
    @model.Rel::GreaterEqual,
    5.0,
    "demand",
  )
  // The plan the outer heuristic handed over.
  let plan : Array[Double] = [2.0, 0.0]
  let bound = @moonopt.solve(@moonopt.relaxation(model))
  let answer = @moonopt.solve(model)
  println("shift cover: min 6a + 5b  s.t.  4a + 2b >= 5,  a and b whole")
  println(
    "  LP bound (relaxation)  : " +
    status_text(bound.status) +
    " " +
    bound.objective.to_string(),
  )
  if answer.status == @moonopt.SolveStatus::Optimal {
    println(
      "  proven optimum         : " +
      answer.objective.to_string() +
      " at (" +
      answer.values[0].to_string() +
      ", " +
      answer.values[1].to_string() +
      ")",
    )
  } else {
    println("  proven optimum         : " + status_text(answer.status))
  }
  let cost = @presolve.objective_value(model, plan)
  let rows = @presolve.max_row_violation(model, plan)
  let bounds = @presolve.max_bound_violation(model, plan)
  println(
    "  the plan you have       : " +
    cost.to_string() +
    " at (" +
    plan[0].to_string() +
    ", " +
    plan[1].to_string() +
    ")",
  )
  println(
    "    feasible              : " +
    (if rows <= 1.0e-9 && bounds <= 1.0e-9 { "yes" } else { "no" }),
  )
  // The gap is the whole point: the plan cannot be better than the bound, so the distance
  // between them is a guarantee about the plan — and it needs no proof of optimality.
  let gap = cost - bound.objective
  println(
    "    gap of that plan      : " +
    gap.to_string() +
    " (" +
    percent(gap, bound.objective) +
    " of the bound)",
  )
  if answer.status == @moonopt.SolveStatus::Optimal {
    let slack = answer.objective - bound.objective
    println(
      "    bound's own looseness : " +
      slack.to_string() +
      " (" +
      percent(slack, answer.objective) +
      " below the optimum)",
    )
  }
}

///|
/// `part / whole` as a percentage, for the two ratios this example prints. `whole` is never
/// zero here (a bound of the zero cost would make the example a different story), but the
/// guard is written anyway rather than assumed away.
fn percent(part : Double, whole : Double) -> String {
  if @core.approx_zero(whole, abs=1.0e-12) {
    "n/a"
  } else {
    (100.0 * part / whole).to_string() + "%"
  }
}

///|
/// Small local helper so the example stays readable.
fn status_text(status : @moonopt.SolveStatus) -> String {
  match status {
    @moonopt.SolveStatus::Optimal => "optimal"
    @moonopt.SolveStatus::Infeasible => "infeasible"
    @moonopt.SolveStatus::Unbounded => "unbounded"
    @moonopt.SolveStatus::NodeLimit => "stopped at the node budget"
    @moonopt.SolveStatus::NotSolved => "not solved"
  }
}