//
//  ScenarioBPlanner.swift
//  SportsTime
//
//  Scenario B: Selected games planning.
//  User selects specific games they MUST see. Those are fixed anchors that cannot be removed.
//
//  Key Features:
//  - Selected games are "anchors" - they MUST appear in every valid route
//  - Sliding window logic when only trip duration (no specific dates) is provided
//  - Additional games from date range can be added if they fit geographically
//
//  Sliding Window Algorithm:
//  When user provides selected games + day span (e.g., 10 days) without specific dates:
//  1. Find first and last selected game dates
//  2. Generate all possible windows of the given duration that contain ALL selected games
//  3. Window 1: Last selected game is on last day
//  4. Window N: First selected game is on first day
//  5. Explore routes for each window, return best options
//
//  Example:
//    Selected games on Jan 5, Jan 8, Jan 12. Day span = 10 days.
//    - Window 1: Jan 3-12 (Jan 12 is last day)
//    - Window 2: Jan 4-13
//    - Window 3: Jan 5-14 (Jan 5 is first day)
//    For each window, find all games and explore routes with selected as anchors.
//

import Foundation
import CoreLocation

/// Scenario B: Selected games planning
/// Input: selected_games, date_range (or trip_duration), optional must_stop
/// Output: Itinerary options connecting all selected games with possible bonus games
///
/// - Expected Behavior:
///   - No selected games → returns .failure with .noValidRoutes
///   - No valid date ranges → returns .failure with .missingDateRange
///   - GameFirst mode → uses sliding windows with gameFirstTripDuration
///   - Explicit dateRange (non-gameFirst) → uses that range directly
///   - All anchor games MUST appear in every valid route
///   - Uses arrivalBeforeGameStart validator for travel segments
///   - No routes satisfy constraints → .failure with .constraintsUnsatisfiable
///   - Success → returns sorted itineraries based on leisureLevel
///
/// - Invariants:
///   - Every returned route contains ALL selected (anchor) games
///   - Anchor games cannot be dropped for geographic convenience
///   - Sliding window always spans from first to last selected game
///   - Date ranges always contain all selected game dates
///
final class ScenarioBPlanner: ScenarioPlanner {

    // MARK: - ScenarioPlanner Protocol

    func plan(request: PlanningRequest) -> ItineraryResult {

        let selectedGames = request.selectedGames

        // ──────────────────────────────────────────────────────────────────
        // Step 1: Validate selected games exist
        // ──────────────────────────────────────────────────────────────────
        if selectedGames.isEmpty {
            return .failure(
                PlanningFailure(
                    reason: .noValidRoutes,
                    violations: [
                        ConstraintViolation(
                            type: .selectedGames,
                            description: "No games selected",
                            severity: .error
                        )
                    ]
                )
            )
        }

        // ──────────────────────────────────────────────────────────────────
        // Step 2: Generate date ranges (sliding window or single range)
        // ──────────────────────────────────────────────────────────────────
        let dateRanges = generateDateRanges(
            selectedGames: selectedGames,
            request: request
        )

        if dateRanges.isEmpty {
            return .failure(
                PlanningFailure(
                    reason: .missingDateRange,
                    violations: [
                        ConstraintViolation(
                            type: .dateRange,
                            description: "Cannot determine valid date range for selected games",
                            severity: .error
                        )
                    ]
                )
            )
        }

        // ──────────────────────────────────────────────────────────────────
        // Step 3: For each date range, find routes with anchors
        // ──────────────────────────────────────────────────────────────────
        let anchorGameIds = Set(selectedGames.map { $0.id })
        let selectedRegions = request.preferences.selectedRegions
        var allItineraryOptions: [ItineraryOption] = []

        for dateRange in dateRanges {
            // Find all games in this date range and selected regions
            let gamesInRange = request.allGames
                .filter { game in
                    // Must be in date range
                    guard dateRange.contains(game.startTime) else { return false }

                    // Must be in selected region (if regions specified)
                    // Note: Anchor games are always included regardless of region
                    if !selectedRegions.isEmpty && !anchorGameIds.contains(game.id) {
                        guard let stadium = request.stadiums[game.stadiumId] else { return false }
                        let gameRegion = Region.classify(longitude: stadium.coordinate.longitude)
                        return selectedRegions.contains(gameRegion)
                    }
                    return true
                }
                .sorted { $0.startTime < $1.startTime }

            // Skip if no games (shouldn't happen if date range is valid)
            guard !gamesInRange.isEmpty else { continue }

            // Verify all selected games are in range
            let selectedInRange = selectedGames.allSatisfy { game in
                dateRange.contains(game.startTime)
            }
            guard selectedInRange else { continue }

            // Find all sensible routes that include the anchor games
            // Uses GameDAGRouter for polynomial-time beam search
            // Run BOTH global and per-region search for diverse routes
            var validRoutes: [[Game]] = []

            // Global beam search (finds cross-region routes)
            let globalRoutes = GameDAGRouter.findAllSensibleRoutes(
                from: gamesInRange,
                stadiums: request.stadiums,
                anchorGameIds: anchorGameIds,
                allowRepeatCities: request.preferences.allowRepeatCities,
                stopBuilder: buildStops
            )
            validRoutes.append(contentsOf: globalRoutes)

            // Per-region beam search (ensures good regional options)
            let regionalRoutes = findRoutesPerRegion(
                games: gamesInRange,
                stadiums: request.stadiums,
                anchorGameIds: anchorGameIds,
                allowRepeatCities: request.preferences.allowRepeatCities
            )
            validRoutes.append(contentsOf: regionalRoutes)

            // Deduplicate
            validRoutes = deduplicateRoutes(validRoutes)

            // Filter routes to only include those containing ALL anchor games
            // This is critical: regional search uses filtered anchors, so we must
            // verify each route satisfies the original anchor constraint
            validRoutes = validRoutes.filter { route in
                let routeGameIds = Set(route.map { $0.id })
                return anchorGameIds.isSubset(of: routeGameIds)
            }

            // Build itineraries for each valid route
            for routeGames in validRoutes {
                let stops = buildStops(from: routeGames, stadiums: request.stadiums)
                guard !stops.isEmpty else { continue }

                // Use shared ItineraryBuilder with arrival time validator
                guard let itinerary = ItineraryBuilder.build(
                    stops: stops,
                    constraints: request.drivingConstraints,
                    logPrefix: "[ScenarioB]",
                    segmentValidator: ItineraryBuilder.arrivalBeforeGameStart()
                ) else {
                    continue
                }

                let selectedCount = routeGames.filter { anchorGameIds.contains($0.id) }.count
                let bonusCount = routeGames.count - selectedCount
                let cities = stops.map { $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: "\(selectedCount) selected + \(bonusCount) bonus games: \(cities)"
                )
                allItineraryOptions.append(option)
            }
        }

        // ──────────────────────────────────────────────────────────────────
        // Step 4: Return ranked results
        // ──────────────────────────────────────────────────────────────────
        if allItineraryOptions.isEmpty {
            return .failure(
                PlanningFailure(
                    reason: .constraintsUnsatisfiable,
                    violations: [
                        ConstraintViolation(
                            type: .geographicSanity,
                            description: "Cannot create a geographically sensible route connecting selected games",
                            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: - Date Range Generation (Sliding Window)

    /// Generates all valid date ranges for the selected games.
    ///
    /// Two modes:
    /// 1. If explicit date range provided: Use it directly (validate selected games fit)
    /// 2. If only trip duration provided: Generate sliding windows
    ///
    /// Sliding Window Logic:
    ///   Selected games: Jan 5, Jan 8, Jan 12. Duration: 10 days.
    ///   - Window must contain all selected games
    ///   - First window: ends on last selected game date (Jan 3-12)
    ///   - Slide forward one day at a time
    ///   - Last window: starts on first selected game date (Jan 5-14)
    ///
    private func generateDateRanges(
        selectedGames: [Game],
        request: PlanningRequest
    ) -> [DateInterval] {

        // For gameFirst mode, ALWAYS use sliding windows with gameFirstTripDuration
        // This generates all possible N-day windows containing the selected games
        let isGameFirstMode = request.preferences.planningMode == .gameFirst
        let duration = isGameFirstMode
            ? request.preferences.gameFirstTripDuration
            : request.preferences.effectiveTripDuration

        // If not gameFirst and explicit date range exists, use it directly
        if !isGameFirstMode, let dateRange = request.dateRange {
            return [dateRange]
        }

        // Use trip duration to create sliding windows
        guard duration > 0 else { return [] }

        // Find the span of selected games
        let sortedGames = selectedGames.sorted { $0.startTime < $1.startTime }
        guard let firstGame = sortedGames.first,
              let lastGame = sortedGames.last else {
            return []
        }

        let firstGameDate = Calendar.current.startOfDay(for: firstGame.startTime)
        let lastGameDate = Calendar.current.startOfDay(for: lastGame.startTime)

        // Calculate how many days the selected games span
        let gameSpanDays = Calendar.current.dateComponents(
            [.day],
            from: firstGameDate,
            to: lastGameDate
        ).day ?? 0

        // If selected games span more days than trip duration, can't fit
        if gameSpanDays >= duration {
            // Just return one window that exactly covers the games
            let start = firstGameDate
            let end = Calendar.current.date(
                byAdding: .day,
                value: gameSpanDays + 1,
                to: start
            ) ?? lastGameDate
            return [DateInterval(start: start, end: end)]
        }

        // Generate sliding windows
        var dateRanges: [DateInterval] = []

        // First window: last selected game is on last day of window
        // Window end = lastGameDate + 1 day (to include the game)
        // Window start = end - duration days
        let firstWindowEnd = Calendar.current.date(
            byAdding: .day,
            value: 1,
            to: lastGameDate
        )!
        let firstWindowStart = Calendar.current.date(
            byAdding: .day,
            value: -duration,
            to: firstWindowEnd
        )!

        // Last window: first selected game is on first day of window
        // Window start = firstGameDate
        let lastWindowStart = firstGameDate

        // Slide from first window to last window
        var currentStart = firstWindowStart
        while currentStart <= lastWindowStart {
            let windowEnd = Calendar.current.date(
                byAdding: .day,
                value: duration,
                to: currentStart
            )!

            let window = DateInterval(start: currentStart, end: windowEnd)
            dateRanges.append(window)

            // Slide forward one day
            currentStart = Calendar.current.date(
                byAdding: .day,
                value: 1,
                to: currentStart
            )!
        }

        return dateRanges
    }

    // MARK: - Stop Building

    /// Converts a list of games into itinerary stops.
    /// 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: [String: 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: String? = 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: String,
        stadiums: [String: 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 same day as last game
        let lastGameDate = sortedGames.last?.gameDate ?? Date()

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

    // MARK: - Regional Route Finding

    /// Finds routes by running beam search separately for each geographic region.
    /// This ensures we get diverse options from East, Central, and West coasts.
    /// For Scenario B, routes must still contain all anchor games.
    private func findRoutesPerRegion(
        games: [Game],
        stadiums: [String: Stadium],
        anchorGameIds: Set<String>,
        allowRepeatCities: Bool
    ) -> [[Game]] {
        // First, determine which region(s) the anchor games are in
        var anchorRegions = Set<Region>()
        for game in games where anchorGameIds.contains(game.id) {
            guard let stadium = stadiums[game.stadiumId] else { continue }
            let coord = stadium.coordinate
            let region = Region.classify(longitude: coord.longitude)
            if region != .crossCountry {
                anchorRegions.insert(region)
            }
        }

        // Partition all games by region
        var gamesByRegion: [Region: [Game]] = [:]
        for game in games {
            guard let stadium = stadiums[game.stadiumId] else { continue }
            let coord = stadium.coordinate
            let region = Region.classify(longitude: coord.longitude)
            if region != .crossCountry {
                gamesByRegion[region, default: []].append(game)
            }
        }

        print("🔍 ScenarioB Regional: Anchor games in regions: \(anchorRegions.map { $0.shortName })")

        // Run beam search for each region that has anchor games
        // (Other regions without anchor games would produce routes that don't satisfy anchors)
        var allRoutes: [[Game]] = []

        for region in anchorRegions {
            guard let regionGames = gamesByRegion[region], !regionGames.isEmpty else { continue }

            // Get anchor games in this region
            let regionAnchorIds = anchorGameIds.filter { anchorId in
                regionGames.contains { $0.id == anchorId }
            }

            let regionRoutes = GameDAGRouter.findAllSensibleRoutes(
                from: regionGames,
                stadiums: stadiums,
                anchorGameIds: regionAnchorIds,
                allowRepeatCities: allowRepeatCities,
                stopBuilder: buildStops
            )

            print("🔍 ScenarioB Regional: \(region.shortName) produced \(regionRoutes.count) routes")
            allRoutes.append(contentsOf: regionRoutes)
        }

        return allRoutes
    }

    // MARK: - Route Deduplication

    /// Removes duplicate routes (routes with identical game IDs).
    private func deduplicateRoutes(_ routes: [[Game]]) -> [[Game]] {
        var seen = Set<String>()
        var unique: [[Game]] = []

        for route in routes {
            let key = route.map { $0.id }.sorted().joined(separator: "-")
            if !seen.contains(key) {
                seen.insert(key)
                unique.append(route)
            }
        }

        return unique
    }

}
