///|
fn write_double_array(builder : StringBuilder, values : Array[Double]) -> Unit {
  builder.write_char('[')
  for index, value in values {
    if index > 0 {
      builder.write_char(',')
    }
    builder.write(value)
  }
  builder.write_char(']')
}

///|
/// Produces a stable row-level prediction audit report.
pub fn prediction_audit_text(audit : PredictionAudit) -> String {
  let builder = StringBuilder()
  builder.write("observations: \{audit.observation_count()}\n")
  builder.write("correct: \{audit.correct_count()}\n")
  builder.write("incorrect: \{audit.incorrect_count()}\n")
  builder.write("accuracy: \{audit.accuracy()}\n")
  builder.write("minimum_winning_margin: \{audit.minimum_winning_margin()}\n")
  builder.write("maximum_winning_margin: \{audit.maximum_winning_margin()}\n")
  builder.write("mean_winning_margin: \{audit.mean_winning_margin()}\n")
  for profile in audit.class_score_profiles() {
    builder.write(
      "class \{profile.class_label()}: positives=\{profile.positive_count()}, negatives=\{profile.negative_count()}, positive_minimum=\{profile.minimum_positive_score()}, positive_maximum=\{profile.maximum_positive_score()}, positive_mean=\{profile.positive_mean_score()}, negative_minimum=\{profile.minimum_negative_score()}, negative_maximum=\{profile.maximum_negative_score()}, negative_mean=\{profile.negative_mean_score()}, separation=\{profile.score_separation()}\n",
    )
  }
  for observation in audit.observations() {
    builder.write(
      "row \{observation.row_index()}: actual=\{observation.actual_class()}, predicted=\{observation.predicted_class()}, winning_score=\{observation.winning_score()}, second_class=\{observation.second_class()}, second_score=\{observation.second_score()}, margin=\{observation.winning_margin()}, correct=\{observation.is_correct()}\n",
    )
  }
  builder.to_string()
}

///|
/// Produces deterministic JSON for row-level prediction evidence.
pub fn prediction_audit_json(audit : PredictionAudit) -> String {
  let builder = StringBuilder()
  builder.write("{\"observation_count\":\{audit.observation_count()}")
  builder.write(",\"correct_count\":\{audit.correct_count()}")
  builder.write(",\"incorrect_count\":\{audit.incorrect_count()}")
  builder.write(",\"accuracy\":\{audit.accuracy()}")
  builder.write(",\"minimum_winning_margin\":\{audit.minimum_winning_margin()}")
  builder.write(",\"maximum_winning_margin\":\{audit.maximum_winning_margin()}")
  builder.write(",\"mean_winning_margin\":\{audit.mean_winning_margin()}")
  builder.write(",\"class_score_profiles\":[")
  for index, profile in audit.class_score_profiles() {
    if index > 0 {
      builder.write_char(',')
    }
    builder.write("{\"class_label\":\{profile.class_label()}")
    builder.write(",\"positive_count\":\{profile.positive_count()}")
    builder.write(",\"negative_count\":\{profile.negative_count()}")
    builder.write(",\"positive_minimum\":\{profile.minimum_positive_score()}")
    builder.write(",\"positive_maximum\":\{profile.maximum_positive_score()}")
    builder.write(",\"positive_mean\":\{profile.positive_mean_score()}")
    builder.write(",\"negative_minimum\":\{profile.minimum_negative_score()}")
    builder.write(",\"negative_maximum\":\{profile.maximum_negative_score()}")
    builder.write(",\"negative_mean\":\{profile.negative_mean_score()}")
    builder.write(",\"score_separation\":\{profile.score_separation()}}")
  }
  builder.write("],\"observations\":[")
  for index, observation in audit.observations() {
    if index > 0 {
      builder.write_char(',')
    }
    builder.write("{\"row_index\":\{observation.row_index()}")
    builder.write(",\"actual_class\":\{observation.actual_class()}")
    builder.write(",\"predicted_class\":\{observation.predicted_class()}")
    builder.write(",\"winning_score\":\{observation.winning_score()}")
    builder.write(",\"second_class\":\{observation.second_class()}")
    builder.write(",\"second_score\":\{observation.second_score()}")
    builder.write(",\"winning_margin\":\{observation.winning_margin()}")
    builder.write(",\"correct\":\{observation.is_correct()}")
    builder.write(",\"scores\":")
    write_double_array(builder, observation.scores())
    builder.write_char('}')
  }
  builder.write("]}")
  builder.to_string()
}

///|
/// Produces a stable ranked permutation-importance report.
pub fn permutation_importance_text(
  importance : PermutationImportance,
) -> String {
  let builder = StringBuilder()
  builder.write("baseline_accuracy: \{importance.baseline_accuracy()}\n")
  builder.write("feature_count: \{importance.feature_count()}\n")
  for feature in importance.ranked_features() {
    builder.write(
      "feature \{feature.feature_index()}: baseline_accuracy=\{feature.baseline_accuracy()}, permuted_accuracy=\{feature.permuted_accuracy()}, accuracy_drop=\{feature.accuracy_drop()}\n",
    )
  }
  builder.to_string()
}

///|
/// Produces deterministic JSON for ranked feature importance.
pub fn permutation_importance_json(
  importance : PermutationImportance,
) -> String {
  let builder = StringBuilder()
  builder.write("{\"baseline_accuracy\":\{importance.baseline_accuracy()}")
  builder.write(",\"feature_count\":\{importance.feature_count()}")
  builder.write(",\"ranked_features\":[")
  for index, feature in importance.ranked_features() {
    if index > 0 {
      builder.write_char(',')
    }
    builder.write("{\"feature_index\":\{feature.feature_index()}")
    builder.write(",\"baseline_accuracy\":\{feature.baseline_accuracy()}")
    builder.write(",\"permuted_accuracy\":\{feature.permuted_accuracy()}")
    builder.write(",\"accuracy_drop\":\{feature.accuracy_drop()}}")
  }
  builder.write("]}")
  builder.to_string()
}

///|
/// Produces stable aggregate diagnostics for an OvR model.
pub fn multiclass_diagnostics_text(
  diagnostics : MulticlassModelDiagnostics,
) -> String {
  let builder = StringBuilder()
  builder.write("class_count: \{diagnostics.class_count()}\n")
  builder.write(
    "total_support_vectors: \{diagnostics.total_support_vectors()}\n",
  )
  builder.write("all_converged: \{diagnostics.all_converged()}\n")
  let counts = diagnostics.support_counts()
  let reports = diagnostics.convergence_reports()
  for index, count in counts {
    let report = reports[index]
    builder.write(
      "model \{index}: support_vectors=\{count}, iterations=\{report.iterations()}, pair_updates=\{report.pair_updates()}, maximum_kkt_violation=\{report.max_kkt_violation()}, dual_objective=\{report.dual_objective()}, converged=\{report.converged()}\n",
    )
  }
  builder.to_string()
}

///|
/// Produces deterministic JSON for every OvR convergence report.
pub fn multiclass_diagnostics_json(
  diagnostics : MulticlassModelDiagnostics,
) -> String {
  let builder = StringBuilder()
  builder.write("{\"class_count\":\{diagnostics.class_count()}")
  builder.write(
    ",\"total_support_vectors\":\{diagnostics.total_support_vectors()}",
  )
  builder.write(",\"all_converged\":\{diagnostics.all_converged()}")
  builder.write(",\"support_counts\":")
  write_int_array(builder, diagnostics.support_counts())
  builder.write(",\"convergence_reports\":[")
  for index, report in diagnostics.convergence_reports() {
    if index > 0 {
      builder.write_char(',')
    }
    builder.write("{\"iterations\":\{report.iterations()}")
    builder.write(",\"pair_updates\":\{report.pair_updates()}")
    builder.write(",\"support_vectors\":\{report.support_vectors()}")
    builder.write(",\"maximum_kkt_violation\":\{report.max_kkt_violation()}")
    builder.write(",\"dual_objective\":\{report.dual_objective()}")
    builder.write(",\"converged\":\{report.converged()}}")
  }
  builder.write("]}")
  builder.to_string()
}

///|
/// Produces a stable summary of positive sample-weight concentration.
pub fn weight_profile_text(profile : WeightProfile) -> String {
  let builder = StringBuilder()
  builder.write("count: \{profile.count()}\n")
  builder.write("minimum: \{profile.minimum()}\n")
  builder.write("maximum: \{profile.maximum()}\n")
  builder.write("total: \{profile.total()}\n")
  builder.write("mean: \{profile.mean()}\n")
  builder.write("effective_sample_size: \{profile.effective_sample_size()}\n")
  builder.to_string()
}

///|
/// Produces deterministic JSON for positive sample-weight evidence.
pub fn weight_profile_json(profile : WeightProfile) -> String {
  let builder = StringBuilder()
  builder.write("{\"count\":\{profile.count()}")
  builder.write(",\"minimum\":\{profile.minimum()}")
  builder.write(",\"maximum\":\{profile.maximum()}")
  builder.write(",\"total\":\{profile.total()}")
  builder.write(",\"mean\":\{profile.mean()}")
  builder.write(
    ",\"effective_sample_size\":\{profile.effective_sample_size()}}",
  )
  builder.to_string()
}

///|
/// Produces stable range and symmetry evidence for a kernel matrix.
pub fn kernel_diagnostics_text(diagnostics : KernelDiagnostics) -> String {
  let builder = StringBuilder()
  builder.write("entry_count: \{diagnostics.entry_count()}\n")
  builder.write("minimum: \{diagnostics.minimum()}\n")
  builder.write("maximum: \{diagnostics.maximum()}\n")
  builder.write("mean: \{diagnostics.mean()}\n")
  builder.write("diagonal_mean: \{diagnostics.diagonal_mean()}\n")
  builder.write(
    "maximum_symmetry_error: \{diagnostics.maximum_symmetry_error()}\n",
  )
  builder.to_string()
}

///|
/// Produces deterministic JSON for kernel matrix diagnostics.
pub fn kernel_diagnostics_json(diagnostics : KernelDiagnostics) -> String {
  let builder = StringBuilder()
  builder.write("{\"entry_count\":\{diagnostics.entry_count()}")
  builder.write(",\"minimum\":\{diagnostics.minimum()}")
  builder.write(",\"maximum\":\{diagnostics.maximum()}")
  builder.write(",\"mean\":\{diagnostics.mean()}")
  builder.write(",\"diagonal_mean\":\{diagnostics.diagonal_mean()}")
  builder.write(
    ",\"maximum_symmetry_error\":\{diagnostics.maximum_symmetry_error()}}",
  )
  builder.to_string()
}