///|
fn calibration_score(
  route : RouteProfile,
  dataset : ObservationDataset,
  congestion : Double,
) -> ErrorSummary {
  let batch = forecast_dataset(dataset, route, congestion~)
  batch.summary
}

///|
fn candidate_compare(
  left : CalibrationCandidate,
  right : CalibrationCandidate,
) -> Int {
  let error_order = compare_double(left.error.mae, right.error.mae)
  if error_order == 0 {
    compare_double(left.congestion, right.congestion)
  } else {
    error_order
  }
}

///|
/// Build evenly spaced congestion candidates, including both endpoints.
pub fn congestion_grid(
  minimum~ : Double,
  maximum~ : Double,
  steps~ : Int,
) -> Array[Double] {
  let low = clamp(minimum, 0.0, 1.0)
  let high = clamp(maximum, low, 1.0)
  let count = if steps < 1 { 1 } else { steps }
  let values : Array[Double] = []
  for index in 0..<=count {
    values.push(low + (high - low) * index.to_double() / count.to_double())
  }
  values
}

///|
/// Evaluate a congestion grid over a labeled dataset.
pub fn grid_calibrate(
  route : RouteProfile,
  dataset : ObservationDataset,
  minimum? : Double = 0.0,
  maximum? : Double = 1.0,
  steps? : Int = 10,
) -> GridCalibration {
  let candidates : Array[CalibrationCandidate] = []
  for congestion in congestion_grid(minimum~, maximum~, steps~) {
    candidates.push({
      congestion,
      error: calibration_score(route, dataset, congestion),
      rank: 0,
    })
  }
  candidates.sort_by(candidate_compare)
  for index in 0.. GridCalibration {
  let center = initial.best.congestion
  let refined = grid_calibrate(
    route,
    dataset,
    minimum=clamp(center - abs_double(radius), 0.0, 1.0),
    maximum=clamp(center + abs_double(radius), 0.0, 1.0),
    steps~,
  )
  if refined.best.error.mae <= initial.best.error.mae {
    refined
  } else {
    initial
  }
}

///|
/// Return the top k candidates after calibration.
pub fn top_candidates(
  calibration : GridCalibration,
  count : Int,
) -> Array[CalibrationCandidate] {
  let result : Array[CalibrationCandidate] = []
  let limit = if count < 0 {
    0
  } else if count > calibration.candidates.length() {
    calibration.candidates.length()
  } else {
    count
  }
  for index in 0.. Double {
  let baseline_error = calibration_score(route, dataset, baseline)
  safe_div(
    baseline_error.mae - calibration.best.error.mae,
    baseline_error.mae,
    0.0,
  )
}

///|
/// Create a calibration dataset from the deterministic demo trace.
pub fn demo_calibration_dataset() -> ObservationDataset {
  let trace = noisy_demo_trace()
  dataset_from_targets(
    trace.observations,
    trace.actual_remaining,
    name="demo-calibration",
  )
}

///|
/// Calibrate the deterministic demo route using the reusable grid API.
pub fn grid_calibrate_demo() -> GridCalibration {
  grid_calibrate(demo_route(), demo_calibration_dataset(), steps=12)
}

///|
/// Return calibration candidates in congestion order.
pub fn candidates_by_congestion(
  calibration : GridCalibration,
) -> Array[CalibrationCandidate] {
  let result = calibration.candidates.copy()
  result.sort_by((left, right) => {
    compare_double(left.congestion, right.congestion)
  })
  result
}

///|
/// Return the error curve as forecast points for charting.
pub fn calibration_error_curve(
  calibration : GridCalibration,
) -> Array[ForecastPoint] {
  let curve : Array[ForecastPoint] = []
  for candidate in calibration.candidates {
    curve.push({ predicted: candidate.congestion, actual: candidate.error.mae })
  }
  curve
}