//
//  TravelEstimator.swift
//  SportsTime
//
//  Shared travel estimation logic used by all scenario planners.
//  Estimating travel from A to B is the same regardless of planning scenario.
//

import Foundation
import CoreLocation

/// Travel estimation utilities for calculating distances and driving times.
///
/// Uses Haversine formula for coordinate-based distance with a road routing factor,
/// or fallback distances when coordinates are unavailable.
///
/// - Constants:
///   - averageSpeedMph: 60 mph
///   - roadRoutingFactor: 1.3 (accounts for roads vs straight-line distance)
///   - fallbackDistanceMiles: 300 miles (when coordinates unavailable)
///
/// - Invariants:
///   - All distance calculations are symmetric: distance(A,B) == distance(B,A)
///   - Road distance >= straight-line distance (roadRoutingFactor >= 1.0)
///   - Travel duration is always distance / averageSpeedMph
///   - Segments exceeding 5x maxDailyDrivingHours return nil (unreachable)
enum TravelEstimator {

    // MARK: - Constants

    private static let averageSpeedMph: Double = 60.0
    private static let roadRoutingFactor: Double = 1.3 // Straight line to road distance
    private static let fallbackDistanceMiles: Double = 300.0

    // MARK: - Travel Estimation

    /// Estimates a travel segment between two ItineraryStops.
    ///
    /// - Parameters:
    ///   - from: Origin stop
    ///   - to: Destination stop
    ///   - constraints: Driving constraints (drivers, hours per day)
    /// - Returns: TravelSegment or nil if unreachable
    ///
    /// - Expected Behavior:
    ///   - With valid coordinates → calculates distance using Haversine * roadRoutingFactor
    ///   - Missing coordinates → uses fallback distance (300 miles)
    ///   - Same city (no coords) → 0 distance, 0 duration
    ///   - Driving hours > 5x maxDailyDrivingHours → returns nil
    ///   - Duration = distance / 60 mph
    ///   - Result distance in meters, duration in seconds
    static func estimate(
        from: ItineraryStop,
        to: ItineraryStop,
        constraints: DrivingConstraints
    ) -> TravelSegment? {

        let distanceMiles = calculateDistanceMiles(from: from, to: to)
        let drivingHours = distanceMiles / averageSpeedMph

        // Maximum allowed: 5 days of driving (matches GameDAGRouter.maxDayLookahead)
        // This allows multi-day cross-country segments like Chicago → Anaheim
        let maxAllowedHours = constraints.maxDailyDrivingHours * 5.0
        if drivingHours > maxAllowedHours {
            return nil
        }

        return TravelSegment(
            fromLocation: from.location,
            toLocation: to.location,
            travelMode: .drive,
            distanceMeters: distanceMiles * 1609.34,
            durationSeconds: drivingHours * 3600
        )
    }

    /// Estimates a travel segment between two LocationInputs.
    ///
    /// - Parameters:
    ///   - from: Origin location
    ///   - to: Destination location
    ///   - constraints: Driving constraints
    /// - Returns: TravelSegment or nil if unreachable/invalid
    ///
    /// - Expected Behavior:
    ///   - Missing from.coordinate → returns nil
    ///   - Missing to.coordinate → returns nil
    ///   - Valid coordinates → calculates distance using Haversine * roadRoutingFactor
    ///   - Driving hours > 5x maxDailyDrivingHours → returns nil
    ///   - Duration = distance / 60 mph
    static func estimate(
        from: LocationInput,
        to: LocationInput,
        constraints: DrivingConstraints
    ) -> TravelSegment? {

        guard let fromCoord = from.coordinate,
              let toCoord = to.coordinate else {
            return nil
        }

        let distanceMeters = haversineDistanceMeters(from: fromCoord, to: toCoord)
        let distanceMiles = distanceMeters * 0.000621371 * roadRoutingFactor
        let drivingHours = distanceMiles / averageSpeedMph

        // Maximum allowed: 5 days of driving (matches GameDAGRouter.maxDayLookahead)
        // This allows multi-day cross-country segments like Chicago → Anaheim
        let maxAllowedHours = constraints.maxDailyDrivingHours * 5.0
        if drivingHours > maxAllowedHours {
            return nil
        }

        return TravelSegment(
            fromLocation: from,
            toLocation: to,
            travelMode: .drive,
            distanceMeters: distanceMeters * roadRoutingFactor,
            durationSeconds: drivingHours * 3600
        )
    }

    // MARK: - Distance Calculations

    /// Calculates road distance in miles between two ItineraryStops.
    ///
    /// - Parameters:
    ///   - from: Origin stop
    ///   - to: Destination stop
    /// - Returns: Distance in miles
    ///
    /// - Expected Behavior:
    ///   - Both have coordinates → Haversine distance * 1.3
    ///   - Either missing coordinates → fallback distance
    ///   - Same city (no coords) → 0 miles
    ///   - Different cities (no coords) → 300 miles
    static func calculateDistanceMiles(
        from: ItineraryStop,
        to: ItineraryStop
    ) -> Double {
        if let fromCoord = from.coordinate,
           let toCoord = to.coordinate {
            return haversineDistanceMiles(from: fromCoord, to: toCoord) * roadRoutingFactor
        }
        return estimateFallbackDistance(from: from, to: to)
    }

    /// Calculates straight-line distance in miles using Haversine formula.
    ///
    /// - Parameters:
    ///   - from: Origin coordinate
    ///   - to: Destination coordinate
    /// - Returns: Straight-line distance in miles
    ///
    /// - Expected Behavior:
    ///   - Same point → 0 miles
    ///   - NYC to Boston → ~190 miles (validates formula accuracy)
    ///   - Symmetric: distance(A,B) == distance(B,A)
    ///   - Uses Earth radius of 3958.8 miles
    static func haversineDistanceMiles(
        from: CLLocationCoordinate2D,
        to: CLLocationCoordinate2D
    ) -> Double {
        let earthRadiusMiles = 3958.8

        let lat1 = from.latitude * .pi / 180
        let lat2 = to.latitude * .pi / 180
        let deltaLat = (to.latitude - from.latitude) * .pi / 180
        let deltaLon = (to.longitude - from.longitude) * .pi / 180

        let a = sin(deltaLat / 2) * sin(deltaLat / 2) +
                cos(lat1) * cos(lat2) *
                sin(deltaLon / 2) * sin(deltaLon / 2)
        let c = 2 * atan2(sqrt(a), sqrt(1 - a))

        return earthRadiusMiles * c
    }

    /// Calculates straight-line distance in meters using Haversine formula.
    ///
    /// - Parameters:
    ///   - from: Origin coordinate
    ///   - to: Destination coordinate
    /// - Returns: Straight-line distance in meters
    ///
    /// - Expected Behavior:
    ///   - Same point → 0 meters
    ///   - Symmetric: distance(A,B) == distance(B,A)
    ///   - Uses Earth radius of 6,371,000 meters
    ///   - haversineDistanceMeters / 1609.34 ≈ haversineDistanceMiles
    static func haversineDistanceMeters(
        from: CLLocationCoordinate2D,
        to: CLLocationCoordinate2D
    ) -> Double {
        let earthRadiusMeters = 6371000.0

        let lat1 = from.latitude * .pi / 180
        let lat2 = to.latitude * .pi / 180
        let deltaLat = (to.latitude - from.latitude) * .pi / 180
        let deltaLon = (to.longitude - from.longitude) * .pi / 180

        let a = sin(deltaLat / 2) * sin(deltaLat / 2) +
                cos(lat1) * cos(lat2) *
                sin(deltaLon / 2) * sin(deltaLon / 2)
        let c = 2 * atan2(sqrt(a), sqrt(1 - a))

        return earthRadiusMeters * c
    }

    /// Fallback distance when coordinates aren't available.
    ///
    /// - Parameters:
    ///   - from: Origin stop
    ///   - to: Destination stop
    /// - Returns: Estimated distance in miles
    ///
    /// - Expected Behavior:
    ///   - Same city → 0 miles
    ///   - Different cities → 300 miles (fallback constant)
    static func estimateFallbackDistance(
        from: ItineraryStop,
        to: ItineraryStop
    ) -> Double {
        if from.city == to.city {
            return 0
        }
        return fallbackDistanceMiles
    }

    // MARK: - Travel Days

    /// Calculates which calendar days a driving segment spans.
    ///
    /// - Parameters:
    ///   - departure: Departure date/time
    ///   - drivingHours: Total driving hours
    /// - Returns: Array of calendar days (start of day) that travel spans
    ///
    /// - Expected Behavior:
    ///   - 0 hours → [departure day]
    ///   - 1-8 hours → [departure day] (1 day)
    ///   - 8.01-16 hours → [departure day, next day] (2 days)
    ///   - 16.01-24 hours → [departure day, +1, +2] (3 days)
    ///   - All dates are normalized to start of day (midnight)
    ///   - Assumes 8 driving hours per day max
    static func calculateTravelDays(
        departure: Date,
        drivingHours: Double
    ) -> [Date] {
        var days: [Date] = []
        let calendar = Calendar.current

        let startDay = calendar.startOfDay(for: departure)
        days.append(startDay)

        // Add days if driving takes multiple days (8 hrs/day max)
        let daysOfDriving = max(1, Int(ceil(drivingHours / 8.0)))
        for dayOffset in 1..<daysOfDriving {
            if let nextDay = calendar.date(byAdding: .day, value: dayOffset, to: startDay) {
                days.append(nextDay)
            }
        }

        return days
    }

}
