///|
/// Numerical policy used by integrations that need explicit tolerances.
pub struct FeasibilityPolicy {
  capacity_tolerance : Double
  time_tolerance : Double
  duration_tolerance : Double
  max_unassigned : Int
} derive(Debug, ToJson)

///|
pub fn FeasibilityPolicy::default() -> FeasibilityPolicy {
  {
    capacity_tolerance: 0.000001,
    time_tolerance: 0.000001,
    duration_tolerance: 0.000001,
    max_unassigned: 0,
  }
}

///|
pub fn FeasibilityPolicy::tune(
  self : FeasibilityPolicy,
  capacity_tolerance? : Double = self.capacity_tolerance,
  time_tolerance? : Double = self.time_tolerance,
  duration_tolerance? : Double = self.duration_tolerance,
  max_unassigned? : Int = self.max_unassigned,
) -> FeasibilityPolicy {
  {
    capacity_tolerance: capacity_tolerance.max(0.0),
    time_tolerance: time_tolerance.max(0.0),
    duration_tolerance: duration_tolerance.max(0.0),
    max_unassigned: max_unassigned.max(0),
  }
}

///|
pub fn FeasibilityPolicy::capacity_tolerance(
  self : FeasibilityPolicy,
) -> Double {
  self.capacity_tolerance
}

///|
pub fn FeasibilityPolicy::time_tolerance(self : FeasibilityPolicy) -> Double {
  self.time_tolerance
}

///|
pub fn FeasibilityPolicy::duration_tolerance(
  self : FeasibilityPolicy,
) -> Double {
  self.duration_tolerance
}

///|
pub fn FeasibilityPolicy::max_unassigned(self : FeasibilityPolicy) -> Int {
  self.max_unassigned
}

///|
pub struct FeasibilityAssessment {
  feasible : Bool
  capacity_violation : Double
  time_violation : Double
  duration_violation : Double
  unassigned_count : Int
  route_count : Int
  validation : ValidationReport
} derive(Debug, ToJson)

///|
pub fn FeasibilityAssessment::is_feasible(self : FeasibilityAssessment) -> Bool {
  self.feasible
}

///|
pub fn FeasibilityAssessment::capacity_violation(
  self : FeasibilityAssessment,
) -> Double {
  self.capacity_violation
}

///|
pub fn FeasibilityAssessment::time_violation(
  self : FeasibilityAssessment,
) -> Double {
  self.time_violation
}

///|
pub fn FeasibilityAssessment::duration_violation(
  self : FeasibilityAssessment,
) -> Double {
  self.duration_violation
}

///|
pub fn FeasibilityAssessment::unassigned_count(
  self : FeasibilityAssessment,
) -> Int {
  self.unassigned_count
}

///|
pub fn FeasibilityAssessment::route_count(self : FeasibilityAssessment) -> Int {
  self.route_count
}

///|
pub fn FeasibilityAssessment::validation(
  self : FeasibilityAssessment,
) -> ValidationReport {
  self.validation
}

///|
/// Evaluates a solution against a caller-provided tolerance policy.
pub fn assess_solution(
  instance : VrpInstance,
  solution : Solution,
  policy : FeasibilityPolicy,
) -> FeasibilityAssessment {
  let validation = validate_solution(instance, solution)
  let capacity = solution.total_capacity_violation()
  let time = solution.total_time_violation()
  let duration = solution.total_duration_violation()
  let unassigned = solution.unassigned_customers().length()
  let within_limits = capacity <= policy.capacity_tolerance &&
    time <= policy.time_tolerance &&
    duration <= policy.duration_tolerance &&
    unassigned <= policy.max_unassigned &&
    solution.route_count() <= instance.vehicle().fleet_size()
  {
    feasible: within_limits && validation.is_valid(),
    capacity_violation: capacity,
    time_violation: time,
    duration_violation: duration,
    unassigned_count: unassigned,
    route_count: solution.route_count(),
    validation,
  }
}

///|
pub fn route_can_accept(
  instance : VrpInstance,
  route : RoutePlan,
  customer_index : Int,
  policy : FeasibilityPolicy,
) -> Bool {
  match insertion_delta(instance, route, customer_index, route.stop_count()) {
    None => false
    Some(_) => {
      let next = route.stops.copy()
      next.push(customer_index)
      let metrics = evaluate_route(instance, next)
      metrics.capacity_violation <= policy.capacity_tolerance &&
      metrics.time_violation <= policy.time_tolerance &&
      metrics.duration_violation <= policy.duration_tolerance
    }
  }
}

///|
pub fn feasible_insertions(
  instance : VrpInstance,
  solution : Solution,
  customer_index : Int,
  config : SolveConfig,
  policy : FeasibilityPolicy,
) -> Array[CandidateInsertion] {
  best_insertions_for_customer(instance, solution, customer_index, config).filter(fn(
      candidate,
    ) {
      let result = candidate.result()
      assess_solution(instance, result, policy).is_feasible() ||
      (
        result.total_capacity_violation() <= policy.capacity_tolerance &&
        result.total_time_violation() <= policy.time_tolerance &&
        result.total_duration_violation() <= policy.duration_tolerance
      )
    },
  )
}