//
//  PlanningModels.swift
//  SportsTime
//
//  Clean model types for trip planning.
//

import Foundation
import CoreLocation

// MARK: - Planning Scenario

/// Exactly one scenario per request. No blending.
enum PlanningScenario: Equatable {
    case scenarioA  // Date range only
    case scenarioB  // Selected games + date range
    case scenarioC  // Start + end locations
    case scenarioD  // Follow team schedule
}

// MARK: - Planning Failure

/// Explicit failure with reason. No silent failures.
struct PlanningFailure: Error {
    let reason: FailureReason
    let violations: [ConstraintViolation]

    enum FailureReason: Equatable {
        case noGamesInRange
        case noValidRoutes
        case missingDateRange
        case missingLocations
        case missingTeamSelection
        case dateRangeViolation(games: [Game])
        case drivingExceedsLimit
        case cannotArriveInTime
        case travelSegmentMissing
        case constraintsUnsatisfiable
        case geographicBacktracking
        case repeatCityViolation(cities: [String])

        static func == (lhs: FailureReason, rhs: FailureReason) -> Bool {
            switch (lhs, rhs) {
            case (.noGamesInRange, .noGamesInRange),
                 (.noValidRoutes, .noValidRoutes),
                 (.missingDateRange, .missingDateRange),
                 (.missingLocations, .missingLocations),
                 (.missingTeamSelection, .missingTeamSelection),
                 (.drivingExceedsLimit, .drivingExceedsLimit),
                 (.cannotArriveInTime, .cannotArriveInTime),
                 (.travelSegmentMissing, .travelSegmentMissing),
                 (.constraintsUnsatisfiable, .constraintsUnsatisfiable),
                 (.geographicBacktracking, .geographicBacktracking):
                return true
            case (.dateRangeViolation(let g1), .dateRangeViolation(let g2)):
                return g1.map { $0.id } == g2.map { $0.id }
            case (.repeatCityViolation(let c1), .repeatCityViolation(let c2)):
                return c1 == c2
            default:
                return false
            }
        }
    }

    init(reason: FailureReason, violations: [ConstraintViolation] = []) {
        self.reason = reason
        self.violations = violations
    }

    var message: String {
        switch reason {
        case .noGamesInRange: return "No games found within the date range"
        case .noValidRoutes: return "No valid routes could be constructed"
        case .missingDateRange: return "Date range is required"
        case .missingLocations: return "Start and end locations are required"
        case .missingTeamSelection: return "Select a team to follow"
        case .dateRangeViolation(let games):
            return "\(games.count) selected game(s) fall outside the date range"
        case .drivingExceedsLimit: return "Driving time exceeds daily limit"
        case .cannotArriveInTime: return "Cannot arrive before game starts"
        case .travelSegmentMissing: return "Travel segment could not be created"
        case .constraintsUnsatisfiable: return "Cannot satisfy all trip constraints"
        case .geographicBacktracking: return "Route requires excessive backtracking"
        case .repeatCityViolation(let cities):
            let cityList = cities.prefix(3).joined(separator: ", ")
            let suffix = cities.count > 3 ? " and \(cities.count - 3) more" : ""
            return "Cannot visit cities on multiple days: \(cityList)\(suffix)"
        }
    }
}

// MARK: - Constraint Violation

struct ConstraintViolation: Equatable {
    let type: ConstraintType
    let description: String
    let severity: ViolationSeverity

    init(constraint: String, detail: String) {
        self.type = .general
        self.description = "\(constraint): \(detail)"
        self.severity = .error
    }

    init(type: ConstraintType, description: String, severity: ViolationSeverity) {
        self.type = type
        self.description = description
        self.severity = severity
    }
}

enum ConstraintType: String, Equatable {
    case dateRange
    case drivingTime
    case geographicSanity
    case mustStop
    case selectedGames
    case gameReachability
    case general
}

enum ViolationSeverity: Equatable {
    case warning
    case error
}

// MARK: - Must Stop Config

struct MustStopConfig {
    static let defaultProximityMiles: Double = 25
    let proximityMiles: Double

    init(proximityMiles: Double = MustStopConfig.defaultProximityMiles) {
        self.proximityMiles = proximityMiles
    }
}

// MARK: - Itinerary Result

/// Either success with ranked options, or explicit failure.
enum ItineraryResult {
    case success([ItineraryOption])
    case failure(PlanningFailure)

    var isSuccess: Bool {
        if case .success = self { return true }
        return false
    }

    var options: [ItineraryOption] {
        if case .success(let opts) = self { return opts }
        return []
    }

    var failure: PlanningFailure? {
        if case .failure(let f) = self { return f }
        return nil
    }
}

// MARK: - Route Candidate

/// Intermediate structure during planning.
struct RouteCandidate {
    let stops: [ItineraryStop]
    let rationale: String
}

// MARK: - Itinerary Option

/// A valid, ranked itinerary option.
struct ItineraryOption: Identifiable {
    let id = UUID()
    let rank: Int
    let stops: [ItineraryStop]
    let travelSegments: [TravelSegment]
    let totalDrivingHours: Double
    let totalDistanceMiles: Double
    let geographicRationale: String

    /// INVARIANT: travelSegments.count == stops.count - 1 (or 0 if single stop)
    var isValid: Bool {
        if stops.count <= 1 { return travelSegments.isEmpty }
        return travelSegments.count == stops.count - 1
    }

    var totalGames: Int {
        stops.reduce(0) { $0 + $1.games.count }
    }

    /// Sorts and ranks itinerary options based on leisure level preference.
    ///
    /// - Parameters:
    ///   - options: The itinerary options to sort
    ///   - leisureLevel: The user's leisure preference
    /// - Returns: Sorted and ranked options (all options, no limit)
    ///
    /// Sorting behavior:
    ///   - Packed: Most games first, then least driving
    ///   - Moderate: Best efficiency (games per driving hour)
    ///   - Relaxed: Least driving first, then fewer games
    static func sortByLeisure(
        _ options: [ItineraryOption],
        leisureLevel: LeisureLevel
    ) -> [ItineraryOption] {
        let sorted = options.sorted { a, b in
            let aGames = a.totalGames
            let bGames = b.totalGames

            switch leisureLevel {
            case .packed:
                // Most games first, then least driving
                if aGames != bGames { return aGames > bGames }
                return a.totalDrivingHours < b.totalDrivingHours

            case .moderate:
                // Best efficiency (games per driving hour)
                let effA = a.totalDrivingHours > 0 ? Double(aGames) / a.totalDrivingHours : Double(aGames)
                let effB = b.totalDrivingHours > 0 ? Double(bGames) / b.totalDrivingHours : Double(bGames)
                if effA != effB { return effA > effB }
                return aGames > bGames

            case .relaxed:
                // Least driving first, then fewer games is fine
                if a.totalDrivingHours != b.totalDrivingHours {
                    return a.totalDrivingHours < b.totalDrivingHours
                }
                return aGames < bGames
            }
        }

        // Re-rank after sorting (no limit - return all options)
        return sorted.enumerated().map { index, option in
            ItineraryOption(
                rank: index + 1,
                stops: option.stops,
                travelSegments: option.travelSegments,
                totalDrivingHours: option.totalDrivingHours,
                totalDistanceMiles: option.totalDistanceMiles,
                geographicRationale: option.geographicRationale
            )
        }
    }
}

// MARK: - Itinerary Stop

/// A stop in the itinerary.
struct ItineraryStop: Identifiable, Hashable {
    let id = UUID()
    let city: String
    let state: String
    let coordinate: CLLocationCoordinate2D?
    let games: [String]  // Canonical game IDs
    let arrivalDate: Date
    let departureDate: Date
    let location: LocationInput
    let firstGameStart: Date?

    var hasGames: Bool { !games.isEmpty }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }

    static func == (lhs: ItineraryStop, rhs: ItineraryStop) -> Bool {
        lhs.id == rhs.id
    }
}

// MARK: - Driving Constraints

/// Driving feasibility constraints.
struct DrivingConstraints {
    let numberOfDrivers: Int
    let maxHoursPerDriverPerDay: Double

    static let `default` = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 8.0)

    var maxDailyDrivingHours: Double {
        Double(numberOfDrivers) * maxHoursPerDriverPerDay
    }

    init(numberOfDrivers: Int = 1, maxHoursPerDriverPerDay: Double = 8.0) {
        self.numberOfDrivers = max(1, numberOfDrivers)
        self.maxHoursPerDriverPerDay = max(1.0, maxHoursPerDriverPerDay)
    }

    init(from preferences: TripPreferences) {
        self.numberOfDrivers = max(1, preferences.numberOfDrivers)
        self.maxHoursPerDriverPerDay = preferences.maxDrivingHoursPerDriver ?? 8.0
    }
}

// MARK: - Timeline Item

/// Unified timeline item for displaying trip itinerary.
/// Enables: Stop → Travel → Stop → Rest → Travel → Stop pattern
enum TimelineItem: Identifiable {
    case stop(ItineraryStop)
    case travel(TravelSegment)
    case rest(RestDay)

    var id: UUID {
        switch self {
        case .stop(let stop): return stop.id
        case .travel(let segment): return segment.id
        case .rest(let rest): return rest.id
        }
    }

    var date: Date? {
        switch self {
        case .stop(let stop): return stop.arrivalDate
        case .travel: return nil  // Travel is location-based, not date-based
        case .rest(let rest): return rest.date
        }
    }

    var isStop: Bool {
        if case .stop = self { return true }
        return false
    }

    var isTravel: Bool {
        if case .travel = self { return true }
        return false
    }

    var isRest: Bool {
        if case .rest = self { return true }
        return false
    }

    /// City/location name for display
    var locationName: String {
        switch self {
        case .stop(let stop): return stop.city
        case .travel(let segment): return "\(segment.fromLocation.name) → \(segment.toLocation.name)"
        case .rest(let rest): return rest.location.name
        }
    }
}

// MARK: - Rest Day

/// A rest day - staying in one place with no travel or games.
struct RestDay: Identifiable, Hashable {
    let id: UUID
    let date: Date
    let location: LocationInput
    let notes: String?

    init(
        id: UUID = UUID(),
        date: Date,
        location: LocationInput,
        notes: String? = nil
    ) {
        self.id = id
        self.date = date
        self.location = location
        self.notes = notes
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }

    static func == (lhs: RestDay, rhs: RestDay) -> Bool {
        lhs.id == rhs.id
    }
}

// MARK: - Timeline Generation

extension ItineraryOption {

    /// Generates a unified timeline from stops and travel segments.
    ///
    /// The timeline interleaves stops and travel in chronological order:
    /// Stop A → Travel A→B → Stop B → Rest Day → Travel B→C → Stop C
    ///
    /// Rest days are inserted when:
    /// - There's a gap between arrival at a stop and departure for next travel
    /// - Multi-day stays at a location without games
    ///
    func generateTimeline() -> [TimelineItem] {
        var timeline: [TimelineItem] = []
        let calendar = Calendar.current

        for (index, stop) in stops.enumerated() {
            // Add the stop
            timeline.append(.stop(stop))

            // Check for rest days at this stop (days between arrival and departure with no games)
            let restDays = calculateRestDays(at: stop, calendar: calendar)
            for restDay in restDays {
                timeline.append(.rest(restDay))
            }

            // Add travel segment to next stop (if not last stop)
            if index < travelSegments.count {
                let segment = travelSegments[index]
                // Travel is location-based - just add the segment
                // Multi-day travel indicated by durationHours > 8
                timeline.append(.travel(segment))
            }
        }

        return timeline
    }

    /// Calculates rest days at a stop (days with no games).
    private func calculateRestDays(
        at stop: ItineraryStop,
        calendar: Calendar
    ) -> [RestDay] {
        // If stop has no games, the entire stay could be considered rest
        // But typically we only insert rest days for multi-day stays

        guard stop.hasGames else {
            // Start/end locations without games - not rest days, just waypoints
            return []
        }

        var restDays: [RestDay] = []

        let arrivalDay = calendar.startOfDay(for: stop.arrivalDate)
        let departureDay = calendar.startOfDay(for: stop.departureDate)

        // If multi-day stay, check each day for games
        var currentDay = arrivalDay
        while currentDay <= departureDay {
            // Skip arrival and departure days (those have the stop itself)
            if currentDay != arrivalDay && currentDay != departureDay {
                // This is a day in between - could be rest or another game day
                // For simplicity, mark in-between days as rest
                let restDay = RestDay(
                    date: currentDay,
                    location: stop.location,
                    notes: "Rest day in \(stop.city)"
                )
                restDays.append(restDay)
            }
            currentDay = calendar.date(byAdding: .day, value: 1, to: currentDay) ?? currentDay
        }

        return restDays
    }

    /// Timeline organized by date for calendar-style display.
    /// Note: Travel segments are excluded as they are location-based, not date-based.
    func timelineByDate() -> [Date: [TimelineItem]] {
        let calendar = Calendar.current
        var byDate: [Date: [TimelineItem]] = [:]

        for item in generateTimeline() {
            // Skip travel items - they don't have dates
            guard let itemDate = item.date else { continue }
            let day = calendar.startOfDay(for: itemDate)
            byDate[day, default: []].append(item)
        }

        return byDate
    }

    /// All dates covered by the itinerary.
    func allDates() -> [Date] {
        let calendar = Calendar.current
        guard let firstStop = stops.first,
              let lastStop = stops.last else { return [] }

        var dates: [Date] = []
        var currentDate = calendar.startOfDay(for: firstStop.arrivalDate)
        let endDate = calendar.startOfDay(for: lastStop.departureDate)

        while currentDate <= endDate {
            dates.append(currentDate)
            currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate) ?? currentDate
        }

        return dates
    }
}

// MARK: - Planning Request

/// Input to the planning engine.
struct PlanningRequest {
    let preferences: TripPreferences
    let availableGames: [Game]
    let teams: [String: Team]      // Keyed by canonical ID
    let stadiums: [String: Stadium]  // Keyed by canonical ID

    // MARK: - Computed Properties for Engine

    /// Games the user explicitly selected (anchors for Scenario B)
    var selectedGames: [Game] {
        availableGames.filter { preferences.mustSeeGameIds.contains($0.id) }
    }

    /// All available games
    var allGames: [Game] {
        availableGames
    }

    /// Start location (for Scenario C)
    var startLocation: LocationInput? {
        preferences.startLocation
    }

    /// End location (for Scenario C)
    var endLocation: LocationInput? {
        preferences.endLocation
    }

    /// Date range as DateInterval
    /// Note: End date is extended to end-of-day to include all games on the last day,
    /// since DateInterval.contains() uses exclusive end boundary.
    var dateRange: DateInterval? {
        guard preferences.endDate > preferences.startDate else { return nil }
        // Extend end date to end of day (23:59:59) to include games on the last day
        let calendar = Calendar.current
        let endOfDay = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: preferences.endDate)
            ?? preferences.endDate
        return DateInterval(start: preferences.startDate, end: endOfDay)
    }

    /// First must-stop location (if any)
    var mustStopLocation: LocationInput? {
        preferences.mustStopLocations.first
    }

    /// Driving constraints
    var drivingConstraints: DrivingConstraints {
        DrivingConstraints(from: preferences)
    }

    /// Get stadium for a game
    func stadium(for game: Game) -> Stadium? {
        stadiums[game.stadiumId]
    }
}
