///|
/// Structural and feasibility diagnostics for instances and solutions.
pub(all) enum ValidationIssue {
  InvalidVehicleCapacity
  InvalidFleetSize
  InvalidRouteDuration
  InvalidDepotWindow
  InvalidCustomerWindow(Int)
  NegativeDemand(Int)
  InvalidCoordinate(Int)
  DuplicateExternalId(Int)
  InvalidDistanceMatrix
  TotalDemandExceedsFleet
  TooManyRoutes
  InvalidRouteCustomer(Int)
  DuplicateAssignment(Int)
  AssignmentOverlap(Int)
  MissingAssignment(Int)
  DuplicateUnassignment(Int)
} derive(Debug, Eq, ToJson)

///|
pub struct ValidationReport {
  valid : Bool
  issues : Array[ValidationIssue]
} derive(Debug, ToJson)

///|
fn ValidationReport::from_issues(
  issues : Array[ValidationIssue],
) -> ValidationReport {
  { valid: issues.is_empty(), issues }
}

///|
pub fn ValidationReport::is_valid(self : ValidationReport) -> Bool {
  self.valid
}

///|
pub fn ValidationReport::issue_count(self : ValidationReport) -> Int {
  self.issues.length()
}

///|
pub fn ValidationReport::issues(
  self : ValidationReport,
) -> Array[ValidationIssue] {
  self.issues.copy()
}

///|
pub fn ValidationReport::has_issue(
  self : ValidationReport,
  issue : ValidationIssue,
) -> Bool {
  self.issues.contains(issue)
}

///|
fn is_bad_number(value : Double) -> Bool {
  value.is_nan() || value.is_inf()
}

///|
fn append_once(
  issues : Array[ValidationIssue],
  issue : ValidationIssue,
) -> Unit {
  if !issues.contains(issue) {
    issues.push(issue)
  }
}

///|
fn validate_distance_matrix(
  instance : VrpInstance,
  issues : Array[ValidationIssue],
) -> Unit {
  let expected = instance.customer_count() + 1
  if instance.distance_matrix.length() != expected {
    append_once(issues, ValidationIssue::InvalidDistanceMatrix)
    return
  }
  for row in instance.distance_matrix {
    if row.length() != expected {
      append_once(issues, ValidationIssue::InvalidDistanceMatrix)
    } else {
      for value in row {
        if is_bad_number(value) || value < 0.0 {
          append_once(issues, ValidationIssue::InvalidDistanceMatrix)
        }
      }
    }
  }
}

///|
/// Checks data invariants without running a solver.
pub fn validate_instance(instance : VrpInstance) -> ValidationReport {
  let issues : Array[ValidationIssue] = []
  if instance.vehicle.capacity <= 0.0 ||
    is_bad_number(instance.vehicle.capacity) {
    issues.push(ValidationIssue::InvalidVehicleCapacity)
  }
  if instance.vehicle.fleet_size <= 0 {
    issues.push(ValidationIssue::InvalidFleetSize)
  }
  if instance.vehicle.max_route_duration <= 0.0 ||
    is_bad_number(instance.vehicle.max_route_duration) {
    issues.push(ValidationIssue::InvalidRouteDuration)
  }
  if instance.depot.ready_time > instance.depot.due_time ||
    is_bad_number(instance.depot.ready_time) ||
    is_bad_number(instance.depot.due_time) {
    issues.push(ValidationIssue::InvalidDepotWindow)
  }
  if is_bad_number(instance.depot.x) || is_bad_number(instance.depot.y) {
    issues.push(ValidationIssue::InvalidCoordinate(instance.depot_index()))
  }
  let identifiers : Array[Int] = []
  let mut total_demand = 0.0
  for index, customer in instance.customers {
    if identifiers.contains(customer.external_id) {
      append_once(
        issues,
        ValidationIssue::DuplicateExternalId(customer.external_id),
      )
    } else {
      identifiers.push(customer.external_id)
    }
    if customer.demand < 0.0 || is_bad_number(customer.demand) {
      append_once(issues, ValidationIssue::NegativeDemand(index))
    }
    total_demand += customer.demand
    if customer.ready_time > customer.due_time ||
      is_bad_number(customer.ready_time) ||
      is_bad_number(customer.due_time) ||
      customer.service_time < 0.0 ||
      is_bad_number(customer.service_time) {
      append_once(issues, ValidationIssue::InvalidCustomerWindow(index))
    }
    if is_bad_number(customer.x) || is_bad_number(customer.y) {
      append_once(issues, ValidationIssue::InvalidCoordinate(index))
    }
  }
  if total_demand >
    instance.vehicle.capacity * Double::from_int(instance.vehicle.fleet_size) {
    append_once(issues, ValidationIssue::TotalDemandExceedsFleet)
  }
  validate_distance_matrix(instance, issues)
  ValidationReport::from_issues(issues)
}

///|
/// Checks that every customer is assigned exactly once or explicitly unassigned.
pub fn validate_solution(
  instance : VrpInstance,
  solution : Solution,
) -> ValidationReport {
  let issues : Array[ValidationIssue] = []
  let customer_count = instance.customer_count()
  let assignments = Array::make(customer_count, 0)
  for route in solution.routes {
    for customer_index in route.stops {
      if customer_index < 0 || customer_index >= customer_count {
        append_once(
          issues,
          ValidationIssue::InvalidRouteCustomer(customer_index),
        )
      } else {
        assignments[customer_index] += 1
        if assignments[customer_index] > 1 {
          append_once(
            issues,
            ValidationIssue::DuplicateAssignment(customer_index),
          )
        }
      }
    }
  }
  if solution.routes.length() > instance.vehicle.fleet_size {
    append_once(issues, ValidationIssue::TooManyRoutes)
  }
  let unassigned_seen : Array[Int] = []
  for customer_index in solution.unassigned {
    if customer_index < 0 || customer_index >= customer_count {
      append_once(issues, ValidationIssue::InvalidRouteCustomer(customer_index))
    } else if unassigned_seen.contains(customer_index) {
      append_once(
        issues,
        ValidationIssue::DuplicateUnassignment(customer_index),
      )
    } else {
      unassigned_seen.push(customer_index)
      if assignments[customer_index] > 0 {
        append_once(issues, ValidationIssue::AssignmentOverlap(customer_index))
      }
    }
  }
  for customer_index in 0.. Array[String] {
  report.issues.map(fn(issue) {
    match issue {
      ValidationIssue::InvalidVehicleCapacity =>
        "vehicle capacity must be positive"
      ValidationIssue::InvalidFleetSize => "fleet size must be positive"
      ValidationIssue::InvalidRouteDuration =>
        "maximum route duration must be positive"
      ValidationIssue::InvalidDepotWindow => "depot time window is invalid"
      ValidationIssue::InvalidCustomerWindow(index) =>
        "customer \{index} has an invalid time or service window"
      ValidationIssue::NegativeDemand(index) =>
        "customer \{index} has negative demand"
      ValidationIssue::InvalidCoordinate(index) =>
        "node \{index} has a non-finite coordinate"
      ValidationIssue::DuplicateExternalId(label) =>
        "external customer id \{label} is duplicated"
      ValidationIssue::InvalidDistanceMatrix => "distance matrix is invalid"
      ValidationIssue::TotalDemandExceedsFleet =>
        "total demand exceeds the available fleet capacity"
      ValidationIssue::TooManyRoutes =>
        "solution uses more routes than the fleet"
      ValidationIssue::InvalidRouteCustomer(index) =>
        "route references customer index \{index}"
      ValidationIssue::DuplicateAssignment(index) =>
        "customer \{index} appears in more than one route position"
      ValidationIssue::AssignmentOverlap(index) =>
        "customer \{index} is both assigned and unassigned"
      ValidationIssue::MissingAssignment(index) =>
        "customer \{index} is neither assigned nor unassigned"
      ValidationIssue::DuplicateUnassignment(index) =>
        "customer \{index} is listed as unassigned more than once"
    }
  })
}