///|
/// Forecast an observation array and score it against the route's historical baseline.
pub fn forecast_batch(
  observations : Array[Observation],
  route : RouteProfile,
  congestion~ : Double,
) -> BatchForecast {
  let results = forecast_route(observations, route)
  let points : Array[ForecastPoint] = []
  let historical_eta = route.historical_eta(congestion~)
  let route_distance = route.total_distance()
  for result in results {
    let baseline = safe_div(
      result.remaining_distance * historical_eta,
      route_distance,
      result.eta_seconds,
    )
    points.push({ predicted: result.eta_seconds, actual: baseline })
  }
  { results, points, summary: summarize_errors(points), skipped: 0 }
}

///|
/// Forecast only the rows with valid observations and retain skipped count.
pub fn forecast_dataset(
  dataset : ObservationDataset,
  route : RouteProfile,
  congestion~ : Double,
) -> BatchForecast {
  let observations : Array[Observation] = []
  let actuals : Array[Double] = []
  let mut skipped = 0
  for row in dataset.rows {
    if row.observation.timestamp < 0.0 || row.observation.distance < 0.0 {
      skipped += 1
    } else {
      observations.push(row.observation)
      match row.actual_remaining {
        Some(value) => actuals.push(value)
        None => actuals.push(0.0)
      }
    }
  }
  let results = forecast_route(observations, route)
  let points : Array[ForecastPoint] = []
  let historical_eta = route.historical_eta(congestion~)
  let route_distance = route.total_distance()
  for index, result in results {
    let actual = if index < actuals.length() && actuals[index] > 0.0 {
      actuals[index]
    } else {
      safe_div(
        result.remaining_distance * historical_eta,
        route_distance,
        result.eta_seconds,
      )
    }
    points.push({ predicted: result.eta_seconds, actual })
  }
  { results, points, summary: summarize_errors(points), skipped }
}

///|
/// Forecast multiple routes with one shared observation trace.
pub fn forecast_many(
  observations : Array[Observation],
  routes : Array[RouteProfile],
  congestion~ : Double,
) -> Array[BatchForecast] {
  let reports : Array[BatchForecast] = []
  for route in routes {
    reports.push(forecast_batch(observations, route, congestion~))
  }
  reports
}

///|
/// Return ETA values from a batch result.
pub fn batch_eta_values(batch : BatchForecast) -> Array[Double] {
  let values : Array[Double] = []
  for result in batch.results {
    values.push(result.eta_seconds)
  }
  values
}

///|
/// Return confidence values from a batch result.
pub fn batch_confidences(batch : BatchForecast) -> Array[Double] {
  let values : Array[Double] = []
  for result in batch.results {
    values.push(result.confidence)
  }
  values
}

///|
/// Re-score a batch against a caller-supplied actual target array.
pub fn rescore_batch(
  batch : BatchForecast,
  actuals : Array[Double],
) -> BatchForecast {
  let points = eta_results_to_points(batch.results, actuals)
  { ..batch, points, summary: summarize_errors(points) }
}

///|
/// Return the first and last ETA values as a compact trend summary.
pub fn eta_trend(batch : BatchForecast) -> (Double, Double, Double) {
  if batch.results.length() == 0 {
    (0.0, 0.0, 0.0)
  } else {
    let first = batch.results[0].eta_seconds
    let last = batch.results[batch.results.length() - 1].eta_seconds
    (first, last, last - first)
  }
}

///|
/// Apply a conservative confidence floor to a result batch.
pub fn apply_confidence_floor(
  batch : BatchForecast,
  floor~ : Double,
) -> BatchForecast {
  let safe_floor = clamp(floor, 0.0, 1.0)
  let results : Array[EtaResult] = []
  for result in batch.results {
    results.push({
      ..result,
      confidence: if result.confidence < safe_floor {
        safe_floor
      } else {
        result.confidence
      },
    })
  }
  { ..batch, results, }
}

///|
/// Partition results by whether their confidence meets a threshold.
pub fn split_by_confidence(
  results : Array[EtaResult],
  threshold~ : Double,
) -> (Array[EtaResult], Array[EtaResult]) {
  let accepted : Array[EtaResult] = []
  let rejected : Array[EtaResult] = []
  for result in results {
    if result.confidence >= threshold {
      accepted.push(result)
    } else {
      rejected.push(result)
    }
  }
  (accepted, rejected)
}