// // DrivingFeasibilityValidator.swift // SportsTime // import Foundation /// Validates driving feasibility based on daily hour limits. /// Priority 4 in the rule hierarchy. /// /// A route is valid ONLY IF: /// - Daily driving time ≤ maxDailyDrivingHours for EVERY day /// - Games are reachable between scheduled times struct DrivingFeasibilityValidator { // MARK: - Validation Result struct ValidationResult { let isValid: Bool let violations: [ConstraintViolation] let failedSegment: SegmentFailure? static let valid = ValidationResult(isValid: true, violations: [], failedSegment: nil) static func drivingExceeded( segment: String, requiredHours: Double, limitHours: Double ) -> ValidationResult { let violation = ConstraintViolation( type: .drivingTime, description: "\(segment) requires \(String(format: "%.1f", requiredHours)) hours driving (limit: \(String(format: "%.1f", limitHours)) hours)", severity: .error ) let failure = SegmentFailure( segmentDescription: segment, requiredHours: requiredHours, limitHours: limitHours ) return ValidationResult(isValid: false, violations: [violation], failedSegment: failure) } static func gameUnreachable( gameId: UUID, arrivalTime: Date, gameTime: Date ) -> ValidationResult { let formatter = DateFormatter() formatter.dateStyle = .short formatter.timeStyle = .short let violation = ConstraintViolation( type: .gameReachability, description: "Cannot arrive (\(formatter.string(from: arrivalTime))) before game starts (\(formatter.string(from: gameTime)))", severity: .error ) return ValidationResult(isValid: false, violations: [violation], failedSegment: nil) } } struct SegmentFailure { let segmentDescription: String let requiredHours: Double let limitHours: Double } // MARK: - Properties let constraints: DrivingConstraints // MARK: - Initialization init(constraints: DrivingConstraints = .default) { self.constraints = constraints } init(from preferences: TripPreferences) { self.constraints = DrivingConstraints(from: preferences) } // MARK: - Validation /// Validates that a single travel segment is feasible within daily driving limits. /// /// - Parameters: /// - drivingHours: Required driving hours for this segment /// - origin: Description of the origin location /// - destination: Description of the destination location /// - Returns: ValidationResult indicating if the segment is feasible func validateSegment( drivingHours: Double, origin: String, destination: String ) -> ValidationResult { let maxDaily = constraints.maxDailyDrivingHours // A segment is valid if it can be completed in one day OR // can be split across multiple days with overnight stops if drivingHours <= maxDaily { return .valid } // Check if it can be reasonably split across multiple days // We allow up to 2 driving days for a single segment let maxTwoDayDriving = maxDaily * 2 if drivingHours <= maxTwoDayDriving { return .valid } // Segment requires more than 2 days of driving - too long return .drivingExceeded( segment: "\(origin) → \(destination)", requiredHours: drivingHours, limitHours: maxDaily ) } /// Validates that a game can be reached in time given departure time and driving duration. /// /// - Parameters: /// - gameId: ID of the game to reach /// - gameTime: When the game starts /// - departureTime: When we leave the previous stop /// - drivingHours: Hours of driving required /// - bufferHours: Buffer time needed before game (default 1 hour for parking, etc.) /// - Returns: ValidationResult indicating if we can reach the game in time func validateGameReachability( gameId: UUID, gameTime: Date, departureTime: Date, drivingHours: Double, bufferHours: Double = 1.0 ) -> ValidationResult { // Calculate arrival time let drivingSeconds = drivingHours * 3600 let bufferSeconds = bufferHours * 3600 let arrivalTime = departureTime.addingTimeInterval(drivingSeconds) let requiredArrivalTime = gameTime.addingTimeInterval(-bufferSeconds) if arrivalTime <= requiredArrivalTime { return .valid } else { return .gameUnreachable( gameId: gameId, arrivalTime: arrivalTime, gameTime: gameTime ) } } /// Validates an entire itinerary's travel segments for driving feasibility. /// /// - Parameter segments: Array of travel segments to validate /// - Returns: ValidationResult with first failure found, or valid if all pass func validateItinerary(segments: [TravelSegment]) -> ValidationResult { for segment in segments { let result = validateSegment( drivingHours: segment.estimatedDrivingHours, origin: segment.fromLocation.name, destination: segment.toLocation.name ) if !result.isValid { return result } } return .valid } /// Calculates how many travel days are needed for a given driving distance. /// /// - Parameter drivingHours: Total hours of driving required /// - Returns: Number of calendar days the travel will span func travelDaysRequired(for drivingHours: Double) -> Int { let maxDaily = constraints.maxDailyDrivingHours if drivingHours <= maxDaily { return 1 } return Int(ceil(drivingHours / maxDaily)) } /// Determines if an overnight stop is needed between two points. /// /// - Parameters: /// - drivingHours: Hours of driving between points /// - departureTime: When we plan to leave /// - Returns: True if an overnight stop is recommended func needsOvernightStop(drivingHours: Double, departureTime: Date) -> Bool { // If driving exceeds daily limit, we need an overnight if drivingHours > constraints.maxDailyDrivingHours { return true } // Also check if arrival would be unreasonably late let arrivalTime = departureTime.addingTimeInterval(drivingHours * 3600) let calendar = Calendar.current let arrivalHour = calendar.component(.hour, from: arrivalTime) // Arriving after 11 PM suggests we should have stopped return arrivalHour >= 23 } }