///|
/// Conservative input guard defaults for embedded applications.
pub fn default_input_guard(route : RouteProfile) -> InputGuardConfig {
  {
    maximum_distance: route.total_distance() * 1.05,
    maximum_timestamp: 7.776e6,
    reject_backwards_time: true,
    reject_backwards_distance: true,
  }
}

///|
/// Return a guard with non-negative finite limits.
pub fn sanitize_input_guard(config : InputGuardConfig) -> InputGuardConfig {
  {
    maximum_distance: if !is_finite_value(config.maximum_distance) ||
      config.maximum_distance < 0.0 {
      0.0
    } else {
      config.maximum_distance
    },
    maximum_timestamp: if !is_finite_value(config.maximum_timestamp) ||
      config.maximum_timestamp < 0.0 {
      0.0
    } else {
      config.maximum_timestamp
    },
    reject_backwards_time: config.reject_backwards_time,
    reject_backwards_distance: config.reject_backwards_distance,
  }
}

///|
/// Guard one observation against finite, range, and monotonicity constraints.
pub fn guard_observation(
  previous : Observation?,
  observation : Observation,
  config : InputGuardConfig,
) -> GuardResult {
  let safe = sanitize_input_guard(config)
  let reasons : Array[String] = []
  if !is_finite_value(observation.timestamp) {
    reasons.push("timestamp is not finite")
  }
  if !is_finite_value(observation.distance) {
    reasons.push("distance is not finite")
  }
  if !is_finite_value(observation.elapsed) {
    reasons.push("elapsed is not finite")
  }
  if observation.timestamp < 0.0 {
    reasons.push("timestamp is negative")
  }
  if observation.distance < 0.0 {
    reasons.push("distance is negative")
  }
  if observation.timestamp > safe.maximum_timestamp {
    reasons.push("timestamp exceeds configured bound")
  }
  if observation.distance > safe.maximum_distance {
    reasons.push("distance exceeds configured bound")
  }
  match previous {
    None => ()
    Some(last) => {
      if safe.reject_backwards_time && observation.timestamp < last.timestamp {
        reasons.push("timestamp moves backwards")
      }
      if safe.reject_backwards_distance && observation.distance < last.distance {
        reasons.push("distance moves backwards")
      }
    }
  }
  { accepted: reasons.length() == 0, observation, reasons }
}

///|
/// Clamp a valid observation to a route distance ceiling.
pub fn clamp_observation_distance(
  observation : Observation,
  maximum_distance : Double,
) -> Observation {
  {
    ..observation,
    distance: clamp(observation.distance, 0.0, maximum_distance),
  }
}

///|
/// Filter a trace through an input guard while retaining reason counts.
pub fn guard_trace(
  observations : Array[Observation],
  config : InputGuardConfig,
) -> (Array[Observation], Array[String]) {
  let accepted : Array[Observation] = []
  let reasons : Array[String] = []
  let mut previous : Observation? = None
  for observation in observations {
    let result = guard_observation(previous, observation, config)
    if result.accepted {
      accepted.push(observation)
      previous = Some(observation)
    } else {
      for reason in result.reasons {
        reasons.push(reason)
      }
    }
  }
  (accepted, reasons)
}

///|
/// Check that an ETA result respects its public numeric contract.
pub fn check_eta_result(result : EtaResult) -> Bool {
  result.eta_seconds >= 0.0 &&
  result.remaining_distance >= 0.0 &&
  result.smoothed_speed >= 0.0 &&
  result.raw_speed >= 0.0 &&
  result.confidence >= 0.0 &&
  result.confidence <= 1.0 &&
  is_finite_value(result.eta_seconds) &&
  is_finite_value(result.confidence)
}

///|
/// Check a whole forecast output for finite and bounded values.
pub fn check_eta_results(results : Array[EtaResult]) -> ContractReport {
  let failures : Array[String] = []
  for index, result in results {
    if !check_eta_result(result) {
      failures.push("result \{index} violates ETA contract")
    }
  }
  { valid: failures.length() == 0, checks: results.length(), failures }
}

///|
/// Check that remaining distance never increases over a result trace.
pub fn check_remaining_distance(results : Array[EtaResult]) -> Bool {
  let mut previous = 1.7976931348623157e308
  let mut valid = true
  for result in results {
    if result.remaining_distance > previous {
      valid = false
    }
    previous = result.remaining_distance
  }
  valid
}

///|
/// Run the standard route forecast contract checks.
pub fn forecast_contract(
  observations : Array[Observation],
  results : Array[EtaResult],
) -> ContractReport {
  let failures : Array[String] = []
  let input_report = validate_observations(observations)
  if input_report.issues.length() > 0 {
    failures.push("input validation failed")
  }
  if !check_remaining_distance(results) {
    failures.push("remaining distance increased")
  }
  let result_report = check_eta_results(results)
  for failure in result_report.failures {
    failures.push(failure)
  }
  {
    valid: failures.length() == 0,
    checks: observations.length() + results.length(),
    failures,
  }
}

///|
/// Render contract failures for an embedding log.
pub fn format_contract(report : ContractReport) -> String {
  let builder = StringBuilder()
  builder.write_string(
    "valid=\{report.valid} checks=\{report.checks} failures=\{report.failures.length()}\n",
  )
  for failure in report.failures {
    builder.write_string(failure)
    builder.write_char('\n')
  }
  builder.to_string()
}

///|
/// Return whether all rows in a dataset can enter the engine.
pub fn dataset_is_guarded(
  dataset : ObservationDataset,
  config : InputGuardConfig,
) -> Bool {
  let (_, reasons) = guard_trace(dataset.observations(), config)
  reasons.length() == 0
}