//
//  ScenarioEPlanner.swift
//  SportsTime
//
//  Scenario E: Team-First planning.
//  User selects teams (not dates), we find optimal trip windows across the season.
//
//  Key Features:
//  - Selected teams determine which home games to visit
//  - Sliding window logic generates all N-day windows across the season
//  - Windows are filtered to those where ALL selected teams have home games
//  - Routes are built and ranked by duration + miles
//
//  Sliding Window Algorithm:
//  1. Fetch all home games for selected teams (full season)
//  2. Generate all N-day windows (N = selectedTeamIds.count * 2)
//  3. Filter to windows where each selected team has at least 1 home game
//  4. Cap at 50 windows (sample if more exist)
//  5. For each valid window, find routes using GameDAGRouter with team games as anchors
//  6. Rank by shortest duration + minimal miles
//  7. Return top 10 results
//
//  Example:
//    User selects: Yankees, Red Sox, Phillies
//    Window duration: 3 * 2 = 6 days
//    Valid windows: Any 6-day span where all 3 teams have a home game
//    Output: Top 10 trip options, ranked by efficiency
//

import Foundation
import CoreLocation

/// Scenario E: Team-First planning
/// Input: selectedTeamIds, sports (for fetching games)
/// Output: Top 10 trip windows with routes visiting all selected teams' home stadiums
///
/// - Expected Behavior:
///   - No selected teams → returns .failure with .missingTeamSelection
///   - Fewer than 2 teams → returns .failure with .missingTeamSelection
///   - No home games found → returns .failure with .noGamesInRange
///   - No valid windows (no overlap) → returns .failure with .noValidRoutes
///   - Each returned route visits ALL selected teams' home stadiums
///   - Routes ranked by duration + miles (shorter is better)
///   - Returns maximum of 10 options
///
/// - Invariants:
///   - All routes contain at least one home game per selected team
///   - Window duration = selectedTeamIds.count * 2 days
///   - Maximum 50 windows evaluated (sampled if more exist)
///   - Maximum 10 results returned
///
final class ScenarioEPlanner: ScenarioPlanner {

    // MARK: - Configuration

    /// Maximum number of windows to evaluate (to prevent combinatorial explosion)
    private let maxWindowsToEvaluate = 50

    /// Maximum number of results to return
    private let maxResultsToReturn = 10

    // MARK: - ScenarioPlanner Protocol

    func plan(request: PlanningRequest) -> ItineraryResult {

        // ──────────────────────────────────────────────────────────────────
        // Step 1: Validate team selection
        // ──────────────────────────────────────────────────────────────────
        let selectedTeamIds = request.preferences.selectedTeamIds

        if selectedTeamIds.count < 2 {
            return .failure(
                PlanningFailure(
                    reason: .missingTeamSelection,
                    violations: [
                        ConstraintViolation(
                            type: .general,
                            description: "Team-First mode requires at least 2 teams selected",
                            severity: .error
                        )
                    ]
                )
            )
        }

        // ──────────────────────────────────────────────────────────────────
        // Step 2: Collect all HOME games for selected teams
        // ──────────────────────────────────────────────────────────────────
        // For Team-First mode, we only care about home games because
        // the user wants to visit each team's home stadium.
        var homeGamesByTeam: [String: [Game]] = [:]
        var allHomeGames: [Game] = []

        for game in request.allGames {
            if selectedTeamIds.contains(game.homeTeamId) {
                homeGamesByTeam[game.homeTeamId, default: []].append(game)
                allHomeGames.append(game)
            }
        }

        // Verify each team has at least one home game
        for teamId in selectedTeamIds {
            if homeGamesByTeam[teamId]?.isEmpty ?? true {
                let teamName = request.teams[teamId]?.fullName ?? teamId
                return .failure(
                    PlanningFailure(
                        reason: .noGamesInRange,
                        violations: [
                            ConstraintViolation(
                                type: .selectedGames,
                                description: "No home games found for \(teamName)",
                                severity: .error
                            )
                        ]
                    )
                )
            }
        }

        print("🔍 ScenarioE Step 2: Found \(allHomeGames.count) total home games across \(selectedTeamIds.count) teams")
        for (teamId, games) in homeGamesByTeam {
            let teamName = request.teams[teamId]?.fullName ?? teamId
            print("🔍   \(teamName): \(games.count) home games")
        }

        // ──────────────────────────────────────────────────────────────────
        // Step 3: Generate sliding windows across the season
        // ──────────────────────────────────────────────────────────────────
        let windowDuration = request.preferences.teamFirstMaxDays
        print("🔍 ScenarioE Step 3: Window duration = \(windowDuration) days")

        let validWindows = generateValidWindows(
            homeGamesByTeam: homeGamesByTeam,
            windowDurationDays: windowDuration,
            selectedTeamIds: selectedTeamIds
        )

        if validWindows.isEmpty {
            return .failure(
                PlanningFailure(
                    reason: .noValidRoutes,
                    violations: [
                        ConstraintViolation(
                            type: .dateRange,
                            description: "No \(windowDuration)-day windows found where all selected teams have home games",
                            severity: .error
                        )
                    ]
                )
            )
        }

        print("🔍 ScenarioE Step 3: Found \(validWindows.count) valid windows")

        // Sample windows if too many exist
        let windowsToEvaluate: [DateInterval]
        if validWindows.count > maxWindowsToEvaluate {
            windowsToEvaluate = sampleWindows(validWindows, count: maxWindowsToEvaluate)
            print("🔍 ScenarioE Step 3: Sampled down to \(windowsToEvaluate.count) windows")
        } else {
            windowsToEvaluate = validWindows
        }

        // ──────────────────────────────────────────────────────────────────
        // Step 4: For each valid window, find routes
        // ──────────────────────────────────────────────────────────────────
        var allItineraryOptions: [ItineraryOption] = []

        for (windowIndex, window) in windowsToEvaluate.enumerated() {
            // Collect games in this window
            // Use one home game per team as anchors (the best one for route efficiency)
            var gamesInWindow: [Game] = []
            var anchorGameIds = Set<String>()

            for teamId in selectedTeamIds {
                guard let teamGames = homeGamesByTeam[teamId] else { continue }

                // Get all home games for this team in the window
                let teamGamesInWindow = teamGames.filter { window.contains($0.startTime) }

                if teamGamesInWindow.isEmpty {
                    // Window doesn't have a game for this team - skip this window
                    // This shouldn't happen since we pre-filtered windows
                    continue
                }

                // Add all games to the pool
                gamesInWindow.append(contentsOf: teamGamesInWindow)

                // Mark the earliest game as anchor (must visit this team)
                if let earliestGame = teamGamesInWindow.sorted(by: { $0.startTime < $1.startTime }).first {
                    anchorGameIds.insert(earliestGame.id)
                }
            }

            // Skip if we don't have anchors for all teams
            guard anchorGameIds.count == selectedTeamIds.count else { continue }

            // Remove duplicate games (same game could be added multiple times if team plays multiple home games)
            let uniqueGames = Array(Set(gamesInWindow)).sorted { $0.startTime < $1.startTime }

            // Find routes using GameDAGRouter with anchor games
            let validRoutes = GameDAGRouter.findAllSensibleRoutes(
                from: uniqueGames,
                stadiums: request.stadiums,
                anchorGameIds: anchorGameIds,
                allowRepeatCities: request.preferences.allowRepeatCities,
                stopBuilder: buildStops
            )

            // Build itineraries for valid routes
            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: "[ScenarioE]",
                    segmentValidator: ItineraryBuilder.arrivalBeforeGameStart()
                ) else {
                    continue
                }

                // Format window dates for rationale
                let dateFormatter = DateFormatter()
                dateFormatter.dateFormat = "MMM d"
                let windowDesc = "\(dateFormatter.string(from: window.start)) - \(dateFormatter.string(from: window.end))"

                let teamsVisited = routeGames.compactMap { game -> String? in
                    if anchorGameIds.contains(game.id) {
                        return request.teams[game.homeTeamId]?.abbreviation ?? game.homeTeamId
                    }
                    return nil
                }.joined(separator: ", ")

                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: "\(windowDesc): \(teamsVisited) - \(cities)"
                )
                allItineraryOptions.append(option)
            }

            // Early exit if we have enough options
            if allItineraryOptions.count >= maxResultsToReturn * 5 {
                print("🔍 ScenarioE: Early exit at window \(windowIndex + 1) with \(allItineraryOptions.count) options")
                break
            }
        }

        // ──────────────────────────────────────────────────────────────────
        // Step 5: Rank and return top results
        // ──────────────────────────────────────────────────────────────────
        if allItineraryOptions.isEmpty {
            return .failure(
                PlanningFailure(
                    reason: .constraintsUnsatisfiable,
                    violations: [
                        ConstraintViolation(
                            type: .geographicSanity,
                            description: "No routes found that can visit all selected teams within driving constraints",
                            severity: .error
                        )
                    ]
                )
            )
        }

        // Deduplicate options (same stops in same order)
        let uniqueOptions = deduplicateOptions(allItineraryOptions)

        // Sort by: shortest duration first, then fewest miles
        let sortedOptions = uniqueOptions.sorted { optionA, optionB in
            // Calculate trip duration in days
            let daysA = tripDurationDays(for: optionA)
            let daysB = tripDurationDays(for: optionB)

            if daysA != daysB {
                return daysA < daysB // Shorter trips first
            }
            return optionA.totalDistanceMiles < optionB.totalDistanceMiles // Fewer miles second
        }

        // Take top N and re-rank
        let topOptions = Array(sortedOptions.prefix(maxResultsToReturn))
        let rankedOptions = topOptions.enumerated().map { index, option in
            ItineraryOption(
                rank: index + 1,
                stops: option.stops,
                travelSegments: option.travelSegments,
                totalDrivingHours: option.totalDrivingHours,
                totalDistanceMiles: option.totalDistanceMiles,
                geographicRationale: option.geographicRationale
            )
        }

        print("🔍 ScenarioE: Returning \(rankedOptions.count) options")

        return .success(rankedOptions)
    }

    // MARK: - Window Generation

    /// Generates all valid N-day windows across the season where ALL selected teams have home games.
    ///
    /// Algorithm:
    /// 1. Find the overall date range (earliest to latest game across all teams)
    /// 2. Slide a window across this range, one day at a time
    /// 3. Keep windows where each team has at least one home game
    ///
    private func generateValidWindows(
        homeGamesByTeam: [String: [Game]],
        windowDurationDays: Int,
        selectedTeamIds: Set<String>
    ) -> [DateInterval] {

        // Find overall date range
        let allGames = homeGamesByTeam.values.flatMap { $0 }
        guard let earliest = allGames.map({ $0.startTime }).min(),
              let latest = allGames.map({ $0.startTime }).max() else {
            return []
        }

        let calendar = Calendar.current
        let earliestDay = calendar.startOfDay(for: earliest)
        let latestDay = calendar.startOfDay(for: latest)

        // Generate sliding windows
        var validWindows: [DateInterval] = []
        var currentStart = earliestDay

        while let windowEnd = calendar.date(byAdding: .day, value: windowDurationDays, to: currentStart),
              windowEnd <= calendar.date(byAdding: .day, value: 1, to: latestDay)! {

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

            // Check if all teams have at least one home game in this window
            let allTeamsHaveGame = selectedTeamIds.allSatisfy { teamId in
                guard let teamGames = homeGamesByTeam[teamId] else { return false }
                return teamGames.contains { window.contains($0.startTime) }
            }

            if allTeamsHaveGame {
                validWindows.append(window)
            }

            // Slide window by one day
            guard let nextStart = calendar.date(byAdding: .day, value: 1, to: currentStart) else {
                break
            }
            currentStart = nextStart
        }

        return validWindows
    }

    /// Samples windows evenly across the season to avoid clustering.
    private func sampleWindows(_ windows: [DateInterval], count: Int) -> [DateInterval] {
        guard windows.count > count else { return windows }

        // Take evenly spaced samples
        var sampled: [DateInterval] = []
        let step = Double(windows.count) / Double(count)

        for i in 0..<count {
            let index = Int(Double(i) * step)
            if index < windows.count {
                sampled.append(windows[index])
            }
        }

        return sampled
    }

    // MARK: - Stop Building

    /// Converts a list of games into itinerary stops.
    /// Groups consecutive games at the same stadium into one stop.
    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: - Deduplication

    /// Removes duplicate itinerary options (same stops in same order).
    private func deduplicateOptions(_ options: [ItineraryOption]) -> [ItineraryOption] {
        var seen = Set<String>()
        var unique: [ItineraryOption] = []

        for option in options {
            // Create key from stop cities in order
            let key = option.stops.map { $0.city }.joined(separator: "-")
            if !seen.contains(key) {
                seen.insert(key)
                unique.append(option)
            }
        }

        return unique
    }

    // MARK: - Trip Duration Calculation

    /// Calculates trip duration in days for an itinerary option.
    private func tripDurationDays(for option: ItineraryOption) -> Int {
        guard let firstStop = option.stops.first,
              let lastStop = option.stops.last else {
            return 0
        }

        let calendar = Calendar.current
        let days = calendar.dateComponents(
            [.day],
            from: calendar.startOfDay(for: firstStop.arrivalDate),
            to: calendar.startOfDay(for: lastStop.departureDate)
        ).day ?? 0

        return max(1, days + 1)
    }
}
