//
//  ScenarioCPlanner.swift
//  SportsTime
//
//  Scenario C: Start + End location planning.
//  User specifies starting city and ending city. We find games along the route.
//
//  Key Features:
//  - Start/End are cities with stadiums from user's selected sports
//  - Directional filtering: stadiums that "generally move toward" the end
//  - When only day span provided (no dates): generate date ranges from games at start/end
//  - Uses GeographicRouteExplorer for sensible route exploration
//  - Returns top 5 options with most games
//
//  Date Range Generation (when only day span provided):
//  1. Find all games at start city's stadiums
//  2. Find all games at end city's stadiums
//  3. For each start game + end game combo within day span: create a date range
//  4. Explore routes for each date range
//
//  Directional Filtering:
//  - Find stadiums that make forward progress from start to end
//  - "Forward progress" = distance to end decreases (with tolerance)
//  - Filter games to only those at directional stadiums
//
//  Example:
//    Start: Chicago, End: New York, Day span: 7 days
//    Start game: Jan 5 at Chicago
//    End game: Jan 10 at New York
//    Date range: Jan 5-10
//    Directional stadiums: Detroit, Cleveland, Pittsburgh (moving east)
//    NOT directional: Minneapolis, St. Louis (moving away from NY)
//

import Foundation
import CoreLocation

/// Scenario C: Directional route planning from start city to end city
/// Input: start_location, end_location, day_span (or date_range)
/// Output: Top 5 itinerary options with games along the directional route
final class ScenarioCPlanner: ScenarioPlanner {

    // MARK: - Configuration

    /// Tolerance for "forward progress" - allow small increases in distance to end
    /// A stadium is "directional" if it doesn't increase distance to end by more than this ratio
    private let forwardProgressTolerance = 0.15  // 15% tolerance

    // MARK: - ScenarioPlanner Protocol

    func plan(request: PlanningRequest) -> ItineraryResult {

        // ──────────────────────────────────────────────────────────────────
        // Step 1: Validate start and end locations
        // ──────────────────────────────────────────────────────────────────
        guard let startLocation = request.startLocation else {
            return .failure(
                PlanningFailure(
                    reason: .missingLocations,
                    violations: [
                        ConstraintViolation(
                            type: .general,
                            description: "Start location is required for Scenario C",
                            severity: .error
                        )
                    ]
                )
            )
        }

        guard let endLocation = request.endLocation else {
            return .failure(
                PlanningFailure(
                    reason: .missingLocations,
                    violations: [
                        ConstraintViolation(
                            type: .general,
                            description: "End location is required for Scenario C",
                            severity: .error
                        )
                    ]
                )
            )
        }

        guard let startCoord = startLocation.coordinate,
              let endCoord = endLocation.coordinate else {
            return .failure(
                PlanningFailure(
                    reason: .missingLocations,
                    violations: [
                        ConstraintViolation(
                            type: .general,
                            description: "Start and end locations must have coordinates",
                            severity: .error
                        )
                    ]
                )
            )
        }

        // ──────────────────────────────────────────────────────────────────
        // Step 2: Find stadiums at start and end cities
        // ──────────────────────────────────────────────────────────────────
        let startStadiums = findStadiumsInCity(
            cityName: startLocation.name,
            stadiums: request.stadiums
        )
        let endStadiums = findStadiumsInCity(
            cityName: endLocation.name,
            stadiums: request.stadiums
        )

        if startStadiums.isEmpty {
            return .failure(
                PlanningFailure(
                    reason: .noGamesInRange,
                    violations: [
                        ConstraintViolation(
                            type: .general,
                            description: "No stadiums found in start city: \(startLocation.name)",
                            severity: .error
                        )
                    ]
                )
            )
        }

        if endStadiums.isEmpty {
            return .failure(
                PlanningFailure(
                    reason: .noGamesInRange,
                    violations: [
                        ConstraintViolation(
                            type: .general,
                            description: "No stadiums found in end city: \(endLocation.name)",
                            severity: .error
                        )
                    ]
                )
            )
        }

        // ──────────────────────────────────────────────────────────────────
        // Step 3: Generate date ranges
        // ──────────────────────────────────────────────────────────────────
        let dateRanges = generateDateRanges(
            startStadiumIds: Set(startStadiums.map { $0.id }),
            endStadiumIds: Set(endStadiums.map { $0.id }),
            allGames: request.allGames,
            request: request
        )

        if dateRanges.isEmpty {
            return .failure(
                PlanningFailure(
                    reason: .missingDateRange,
                    violations: [
                        ConstraintViolation(
                            type: .dateRange,
                            description: "No valid date ranges found with games at both start and end cities",
                            severity: .error
                        )
                    ]
                )
            )
        }

        // ──────────────────────────────────────────────────────────────────
        // Step 4: Find directional stadiums (moving from start toward end)
        // ──────────────────────────────────────────────────────────────────
        let directionalStadiums = findDirectionalStadiums(
            from: startCoord,
            to: endCoord,
            stadiums: request.stadiums
        )

        // ──────────────────────────────────────────────────────────────────
        // Step 5: For each date range, explore routes
        // ──────────────────────────────────────────────────────────────────
        var allItineraryOptions: [ItineraryOption] = []

        for dateRange in dateRanges {
            // Find games at directional stadiums within date range
            let gamesInRange = request.allGames
                .filter { dateRange.contains($0.startTime) }
                .filter { directionalStadiums.contains($0.stadiumId) }
                .sorted { $0.startTime < $1.startTime }

            guard !gamesInRange.isEmpty else { continue }

            // Use GameDAGRouter for polynomial-time beam search
            let validRoutes = GameDAGRouter.findAllSensibleRoutes(
                from: gamesInRange,
                stadiums: request.stadiums,
                anchorGameIds: [],  // No anchors in Scenario C
                stopBuilder: buildStops
            )

            // Build itineraries for each valid route
            for routeGames in validRoutes {
                let stops = buildStopsWithEndpoints(
                    start: startLocation,
                    end: endLocation,
                    games: routeGames,
                    stadiums: request.stadiums
                )
                guard !stops.isEmpty else { continue }

                // Validate monotonic progress toward end
                guard validateMonotonicProgress(
                    stops: stops,
                    toward: endCoord
                ) else {
                    continue
                }

                // Use shared ItineraryBuilder
                guard let itinerary = ItineraryBuilder.build(
                    stops: stops,
                    constraints: request.drivingConstraints,
                    logPrefix: "[ScenarioC]"
                ) else {
                    continue
                }

                let gameCount = routeGames.count
                let cities = stops.compactMap { $0.games.isEmpty ? nil : $0.city }.joined(separator: " → ")

                let option = ItineraryOption(
                    rank: 0,  // Will re-rank later
                    stops: itinerary.stops,
                    travelSegments: itinerary.travelSegments,
                    totalDrivingHours: itinerary.totalDrivingHours,
                    totalDistanceMiles: itinerary.totalDistanceMiles,
                    geographicRationale: "\(startLocation.name) → \(gameCount) games → \(endLocation.name): \(cities)"
                )
                allItineraryOptions.append(option)
            }
        }

        // ──────────────────────────────────────────────────────────────────
        // Step 6: Return top 5 ranked results
        // ──────────────────────────────────────────────────────────────────
        if allItineraryOptions.isEmpty {
            return .failure(
                PlanningFailure(
                    reason: .noValidRoutes,
                    violations: [
                        ConstraintViolation(
                            type: .geographicSanity,
                            description: "No valid directional routes found from \(startLocation.name) to \(endLocation.name)",
                            severity: .error
                        )
                    ]
                )
            )
        }

        // Sort and rank based on leisure level
        let leisureLevel = request.preferences.leisureLevel
        let rankedOptions = ItineraryOption.sortByLeisure(
            allItineraryOptions,
            leisureLevel: leisureLevel
        )

        return .success(Array(rankedOptions))
    }

    // MARK: - Stadium Finding

    /// Finds all stadiums in a given city (case-insensitive match).
    private func findStadiumsInCity(
        cityName: String,
        stadiums: [UUID: Stadium]
    ) -> [Stadium] {
        let normalizedCity = cityName.lowercased().trimmingCharacters(in: .whitespaces)
        return stadiums.values.filter { stadium in
            stadium.city.lowercased().trimmingCharacters(in: .whitespaces) == normalizedCity
        }
    }

    /// Finds stadiums that make forward progress from start to end.
    ///
    /// A stadium is "directional" if visiting it doesn't significantly increase
    /// the distance to the end point. This filters out stadiums that would
    /// require backtracking.
    ///
    /// Algorithm:
    ///   1. Calculate distance from start to end
    ///   2. For each stadium, calculate: distance(start, stadium) + distance(stadium, end)
    ///   3. If this "detour distance" is reasonable (within tolerance), include it
    ///
    /// The tolerance allows for stadiums slightly off the direct path.
    ///
    private func findDirectionalStadiums(
        from start: CLLocationCoordinate2D,
        to end: CLLocationCoordinate2D,
        stadiums: [UUID: Stadium]
    ) -> Set<UUID> {
        let directDistance = distanceBetween(start, end)

        // Allow detours up to 50% longer than direct distance
        let maxDetourDistance = directDistance * 1.5

        var directionalIds: Set<UUID> = []

        for (id, stadium) in stadiums {
            let stadiumCoord = stadium.coordinate

            // Calculate the detour: start → stadium → end
            let toStadium = distanceBetween(start, stadiumCoord)
            let fromStadium = distanceBetween(stadiumCoord, end)
            let detourDistance = toStadium + fromStadium

            // Also check that stadium is making progress (closer to end than start is)
            let distanceToEnd = distanceBetween(stadiumCoord, end)

            // Stadium should be within the "cone" from start to end
            // Either closer to end than start, or the detour is acceptable
            if detourDistance <= maxDetourDistance {
                // Additional check: don't include if it's behind the start point
                // (i.e., distance to end is greater than original distance)
                if distanceToEnd <= directDistance * (1 + forwardProgressTolerance) {
                    // Final check: exclude stadiums that are beyond the end point
                    // A stadium is beyond the end if it's farther from start than end is
                    if toStadium <= directDistance * (1 + forwardProgressTolerance) {
                        directionalIds.insert(id)
                    }
                }
            }
        }

        return directionalIds
    }

    // MARK: - Date Range Generation

    /// Generates date ranges for Scenario C.
    ///
    /// Two modes:
    /// 1. Explicit date range provided: Use it directly
    /// 2. Only day span provided: Find game combinations at start/end cities
    ///
    /// For mode 2:
    ///   - Find all games at start city stadiums
    ///   - Find all games at end city stadiums
    ///   - For each (start_game, end_game) pair where end_game - start_game <= day_span:
    ///     Create a date range from start_game.date to end_game.date
    ///
    private func generateDateRanges(
        startStadiumIds: Set<UUID>,
        endStadiumIds: Set<UUID>,
        allGames: [Game],
        request: PlanningRequest
    ) -> [DateInterval] {

        // If explicit date range exists, use it
        if let dateRange = request.dateRange {
            return [dateRange]
        }

        // Otherwise, use day span to find valid combinations
        let daySpan = request.preferences.effectiveTripDuration
        guard daySpan > 0 else { return [] }

        // Find games at start and end cities
        let startGames = allGames
            .filter { startStadiumIds.contains($0.stadiumId) }
            .sorted { $0.startTime < $1.startTime }

        let endGames = allGames
            .filter { endStadiumIds.contains($0.stadiumId) }
            .sorted { $0.startTime < $1.startTime }

        if startGames.isEmpty || endGames.isEmpty {
            return []
        }

        // Generate all valid (start_game, end_game) combinations
        var dateRanges: [DateInterval] = []
        let calendar = Calendar.current

        for startGame in startGames {
            let startDate = calendar.startOfDay(for: startGame.startTime)

            for endGame in endGames {
                let endDate = calendar.startOfDay(for: endGame.startTime)

                // End must be after start
                guard endDate >= startDate else { continue }

                // Calculate days between
                let daysBetween = calendar.dateComponents([.day], from: startDate, to: endDate).day ?? 0

                // Must be within day span
                guard daysBetween < daySpan else { continue }

                // Create date range (end date + 1 day to include the end game)
                let rangeEnd = calendar.date(byAdding: .day, value: 1, to: endDate) ?? endDate
                let range = DateInterval(start: startDate, end: rangeEnd)

                // Avoid duplicate ranges
                if !dateRanges.contains(where: { $0.start == range.start && $0.end == range.end }) {
                    dateRanges.append(range)
                }
            }
        }

        return dateRanges
    }

    // MARK: - Stop Building

    /// Converts games to stops (used by GeographicRouteExplorer callback).
    /// Groups consecutive games at the same stadium into one stop.
    /// Creates separate stops when visiting the same city with other cities in between.
    private func buildStops(
        from games: [Game],
        stadiums: [UUID: Stadium]
    ) -> [ItineraryStop] {
        guard !games.isEmpty else { return [] }

        // Sort games chronologically
        let sortedGames = games.sorted { $0.startTime < $1.startTime }

        // Group consecutive games at the same stadium
        var stops: [ItineraryStop] = []
        var currentStadiumId: UUID? = nil
        var currentGames: [Game] = []

        for game in sortedGames {
            if game.stadiumId == currentStadiumId {
                // Same stadium as previous game - add to current group
                currentGames.append(game)
            } else {
                // Different stadium - finalize previous stop (if any) and start new one
                if let stadiumId = currentStadiumId, !currentGames.isEmpty {
                    if let stop = createStop(from: currentGames, stadiumId: stadiumId, stadiums: stadiums) {
                        stops.append(stop)
                    }
                }
                currentStadiumId = game.stadiumId
                currentGames = [game]
            }
        }

        // Don't forget the last group
        if let stadiumId = currentStadiumId, !currentGames.isEmpty {
            if let stop = createStop(from: currentGames, stadiumId: stadiumId, stadiums: stadiums) {
                stops.append(stop)
            }
        }

        return stops
    }

    /// Creates an ItineraryStop from a group of games at the same stadium.
    private func createStop(
        from games: [Game],
        stadiumId: UUID,
        stadiums: [UUID: Stadium]
    ) -> ItineraryStop? {
        guard !games.isEmpty else { return nil }

        let sortedGames = games.sorted { $0.startTime < $1.startTime }
        let stadium = stadiums[stadiumId]
        let city = stadium?.city ?? "Unknown"
        let state = stadium?.state ?? ""
        let coordinate = stadium?.coordinate

        let location = LocationInput(
            name: city,
            coordinate: coordinate,
            address: stadium?.fullAddress
        )

        // departureDate is day AFTER last game (we leave the next morning)
        let lastGameDate = sortedGames.last?.gameDate ?? Date()
        let departureDateValue = Calendar.current.date(byAdding: .day, value: 1, to: lastGameDate) ?? lastGameDate

        return ItineraryStop(
            city: city,
            state: state,
            coordinate: coordinate,
            games: sortedGames.map { $0.id },
            arrivalDate: sortedGames.first?.gameDate ?? Date(),
            departureDate: departureDateValue,
            location: location,
            firstGameStart: sortedGames.first?.startTime
        )
    }

    /// Builds stops with start and end location endpoints.
    private func buildStopsWithEndpoints(
        start: LocationInput,
        end: LocationInput,
        games: [Game],
        stadiums: [UUID: Stadium]
    ) -> [ItineraryStop] {

        var stops: [ItineraryStop] = []

        // Start stop (no games)
        let startArrival = games.first?.gameDate.addingTimeInterval(-86400) ?? Date()
        let startStop = ItineraryStop(
            city: start.name,
            state: "",
            coordinate: start.coordinate,
            games: [],
            arrivalDate: startArrival,
            departureDate: startArrival,
            location: start,
            firstGameStart: nil
        )
        stops.append(startStop)

        // Game stops
        let gameStops = buildStops(from: games, stadiums: stadiums)
        stops.append(contentsOf: gameStops)

        // End stop (no games)
        let endArrival = games.last?.gameDate.addingTimeInterval(86400) ?? Date()
        let endStop = ItineraryStop(
            city: end.name,
            state: "",
            coordinate: end.coordinate,
            games: [],
            arrivalDate: endArrival,
            departureDate: endArrival,
            location: end,
            firstGameStart: nil
        )
        stops.append(endStop)

        return stops
    }

    // MARK: - Monotonic Progress Validation

    /// Validates that the route makes generally forward progress toward the end.
    ///
    /// Each stop should be closer to (or not significantly farther from) the end
    /// than the previous stop. Small detours are allowed within tolerance.
    ///
    private func validateMonotonicProgress(
        stops: [ItineraryStop],
        toward end: CLLocationCoordinate2D
    ) -> Bool {

        var previousDistance: Double?

        for stop in stops {
            guard let stopCoord = stop.coordinate else { continue }

            let currentDistance = distanceBetween(stopCoord, end)

            if let prev = previousDistance {
                // Allow increases up to tolerance percentage
                let allowedIncrease = prev * forwardProgressTolerance
                if currentDistance > prev + allowedIncrease {
                    return false
                }
            }

            previousDistance = currentDistance
        }

        return true
    }

    // MARK: - Geometry Helpers

    /// Distance between two coordinates in miles using Haversine formula.
    private func distanceBetween(
        _ coord1: CLLocationCoordinate2D,
        _ coord2: CLLocationCoordinate2D
    ) -> Double {
        let earthRadiusMiles = 3958.8

        let lat1 = coord1.latitude * .pi / 180
        let lat2 = coord2.latitude * .pi / 180
        let deltaLat = (coord2.latitude - coord1.latitude) * .pi / 180
        let deltaLon = (coord2.longitude - coord1.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
    }
}
