import Foundation

// Response shape from route-explorer.com /api/flight-search proxy.
// All endpoints (/route, /departures, /flight, /schedule) return a
// `{ json: { connections: [...], appendix?: {...} } }` envelope.

struct RouteExplorerResponse: Decodable, Sendable {
    let json: Body

    struct Body: Decodable, Sendable {
        let connections: [RouteConnection]
        let appendix: RouteAppendix?
    }
}

struct RouteConnection: Decodable, Identifiable, Sendable {
    var id: String {
        flights.map { $0.id }.joined(separator: "→")
    }
    let durationMinutes: Int
    let score: Int
    let flights: [RouteFlight]

    var stopCount: Int { max(0, flights.count - 1) }

    /// Total layover time in minutes (sum across all connection points).
    var totalLayoverMinutes: Int {
        guard flights.count >= 2 else { return 0 }
        var total = 0
        for i in 1..<flights.count {
            let arr = flights[i - 1].arrival.dateTime
            let dep = flights[i].departure.dateTime
            total += max(0, Int(dep.timeIntervalSince(arr) / 60))
        }
        return total
    }

    /// Distinct carrier IATAs across all legs.
    var carrierIatas: [String] {
        var seen = Set<String>()
        var ordered: [String] = []
        for f in flights where !seen.contains(f.carrierIata) {
            seen.insert(f.carrierIata)
            ordered.append(f.carrierIata)
        }
        return ordered
    }
}

struct RouteFlight: Decodable, Identifiable, Sendable {
    let id: String
    let carrierIata: String
    let carrierIcao: String?
    let flightNumber: Int
    let flightSuffix: String?
    let departure: RouteEndpoint
    let arrival: RouteEndpoint
    let durationMinutes: Int
    let equipmentIata: String?
    let serviceType: String?
    let isCodeshare: Bool
    let stops: Int
    let stopCodes: [String]?
    let totalSeats: Int?
    let classes: RouteCabins?
    let inFlightService: [Int]?
    let isWetlease: Bool?
    let codeshares: [RouteCodeshare]?

    /// Combined display string: "AA2178" or "AA2178A" with suffix.
    var displayFlightNumber: String {
        "\(carrierIata)\(flightNumber)\(flightSuffix ?? "")"
    }
}

struct RouteEndpoint: Decodable, Sendable {
    let airportIata: String
    let dateTime: Date
    let terminal: String?
}

struct RouteCabins: Decodable, Sendable {
    let first: RouteCabin?
    let business: RouteCabin?
    let premiumEconomy: RouteCabin?
    let economy: RouteCabin?

    /// As a CabinClass option set (matches existing app model).
    var asCabinClass: CabinClass {
        var result = CabinClass()
        if (first?.seats ?? 0) > 0 { result.insert(.first) }
        if (business?.seats ?? 0) > 0 { result.insert(.business) }
        if (premiumEconomy?.seats ?? 0) > 0 { result.insert(.premiumEcon) }
        if (economy?.seats ?? 0) > 0 { result.insert(.economy) }
        return result
    }
}

struct RouteCabin: Decodable, Sendable {
    let seats: Int
    let mealCodes: [String]?
}

struct RouteCodeshare: Decodable, Sendable {
    let carrierIata: String?
    let carrierIcao: String?
    let flightNumber: Int?
}

// MARK: - Appendix (reference data attached when includeAppendix=true)

struct RouteAppendix: Decodable, Sendable {
    let airports: [RouteAppendixAirport]
    let airlines: [RouteAppendixAirline]
    let equipment: [RouteAppendixEquipment]

    func airline(iata: String) -> RouteAppendixAirline? {
        airlines.first { $0.iataCode == iata }
    }

    func equipment(iata: String) -> RouteAppendixEquipment? {
        equipment.first { $0.iataCode == iata }
    }

    func airport(iata: String) -> RouteAppendixAirport? {
        airports.first { $0.iataCode == iata }
    }
}

struct RouteAppendixAirport: Decodable, Sendable {
    let iataCode: String
    let icaoCode: String?
    let name: String?
    let cityName: String?
    let countryCode: String?
    let latitude: Double?
    let longitude: Double?
    let timezone: String?
}

struct RouteAppendixAirline: Decodable, Sendable {
    let iataCode: String
    let icaoCode: String?
    let name: String?
}

struct RouteAppendixEquipment: Decodable, Sendable {
    let iataCode: String
    let icaoCode: String?
    let generalCode: String?
    let name: String?
    let equipmentType: String?
    let bodyType: String?
}

// MARK: - Search result

/// Response from `/schedule` — flat list of operating records for one
/// (carrier, flightNumber, date). Different envelope from `/route` /
/// `/departures` which return nested `connections[]`.
struct RouteExplorerScheduleResponse: Decodable, Sendable {
    let json: Body
    struct Body: Decodable, Sendable {
        let flights: [RouteFlight]
        let appendix: RouteAppendix?
    }
}

struct RouteSearchResult: Sendable {
    let connections: [RouteConnection]
    let appendix: RouteAppendix?
}

/// Sort options for results lists. All applied client-side after fetch —
/// upstream is always told to sort by `departure_time` so we get a stable
/// base order, then we reorder in `RoutePlannerView` (or in
/// `filteredFlights` for the departures list).
enum RouteSortOption: String, CaseIterable, Sendable {
    case departureEarliest
    case departureLatest
    case fewestStops
    case mostStops

    var label: String {
        switch self {
        case .departureEarliest: return "Departure Earliest"
        case .departureLatest:   return "Departure Latest"
        case .fewestStops:       return "Fewest Stops"
        case .mostStops:         return "Most Stops"
        }
    }

    /// String value the upstream API accepts. `nil` → option is purely
    /// client-side; the client falls back to `departure_time`.
    var apiValue: String? {
        switch self {
        case .departureEarliest: return "departure_time"
        default:                 return nil
        }
    }

    /// Sort options shown in connection mode (TO is set).
    static let connectionOptions: [RouteSortOption] = [
        .departureEarliest, .departureLatest, .fewestStops, .mostStops
    ]

    /// Sort options shown in "where can I go?" mode (TO is empty). All
    /// results are direct, so the stop-count options aren't meaningful —
    /// keep just the two time-based options.
    static let departureOptions: [RouteSortOption] = [
        .departureEarliest, .departureLatest
    ]
}

// MARK: - Client-side sort comparators

extension RouteConnection {
    /// First-leg departure time, used as a stable tiebreaker so equal-stop
    /// connections still come out chronologically within their group.
    var firstDeparture: Date {
        flights.first?.departure.dateTime ?? .distantFuture
    }
}

extension Array where Element == RouteConnection {
    func sorted(by option: RouteSortOption) -> [RouteConnection] {
        switch option {
        case .departureEarliest:
            return sorted { $0.firstDeparture < $1.firstDeparture }
        case .departureLatest:
            return sorted { $0.firstDeparture > $1.firstDeparture }
        case .fewestStops:
            return sorted {
                if $0.stopCount != $1.stopCount {
                    return $0.stopCount < $1.stopCount
                }
                return $0.firstDeparture < $1.firstDeparture
            }
        case .mostStops:
            return sorted {
                if $0.stopCount != $1.stopCount {
                    return $0.stopCount > $1.stopCount
                }
                return $0.firstDeparture < $1.firstDeparture
            }
        }
    }
}

extension Array where Element == RouteFlight {
    /// Apply a sort to a flat list of legs (the where-can-I-go results
    /// after window filtering). Stop-count options collapse to chronological
    /// since departures are always single-leg.
    func sorted(by option: RouteSortOption) -> [RouteFlight] {
        switch option {
        case .departureEarliest, .fewestStops, .mostStops:
            return sorted { $0.departure.dateTime < $1.departure.dateTime }
        case .departureLatest:
            return sorted { $0.departure.dateTime > $1.departure.dateTime }
        }
    }
}

// MARK: - Sheet payload

/// Identifiable bundle of everything FlightLoadDetailView needs from a
/// RouteFlight tap. Use this as a single `@State` so `.sheet(item:)` sees
/// schedule + origin + destination + date atomically. Separate @State
/// properties race: setting `selectedFlight` non-nil materializes the sheet
/// before the other writes settle, and the sheet captures empty strings —
/// which then hit the AA endpoint as `originAirportCode=&destinationAirportCode=`
/// and bounce as HTTP 400.
struct RouteLoadDetailRequest: Identifiable {
    let id = UUID()
    let schedule: FlightSchedule
    let departureCode: String
    let arrivalCode: String
    let date: Date
}

/// Identifiable wrapper for presenting a multi-leg connection as a sheet.
/// Carries the connection itself plus the appendix (so the view can resolve
/// airline / equipment names and airport metadata).
struct ConnectionLoadRequest: Identifiable {
    let id = UUID()
    let connection: RouteConnection
    let appendix: RouteAppendix?
}

// MARK: - Bridge to existing FlightSchedule (for FlightLoadDetailView reuse)

extension RouteFlight {
    /// Build a FlightSchedule from this leg so the existing FlightLoadDetailView
    /// can drive its load lookup. Only the fields the detail view actually
    /// reads are populated meaningfully — others get sensible defaults.
    func toFlightSchedule(appendix: RouteAppendix?, on date: Date) -> FlightSchedule {
        let airlineName = appendix?.airline(iata: carrierIata)?.name ?? carrierIata
        let aircraftName = appendix?.equipment(iata: equipmentIata ?? "")?.name ?? equipmentIata ?? ""

        let airline = Airline(
            id: carrierIata,
            name: airlineName,
            iata: carrierIata,
            // Flightconnections logo filename convention; falls back to placeholder.
            logoFilename: "\(carrierIata.lowercased()).png"
        )

        // The dateTime values from the API already carry their local offset, so
        // formatting in the airport's local time means using the wall-clock part.
        let depTime = Self.localTimeString(from: departure.dateTime, isoZoneFromIATA: departure.airportIata)
        let arrTime = Self.localTimeString(from: arrival.dateTime, isoZoneFromIATA: arrival.airportIata)

        var calendar = Calendar(identifier: .gregorian)
        calendar.timeZone = TimeZone(identifier: "UTC")!
        let startOfDay = calendar.startOfDay(for: date)
        let weekday = Calendar.current.component(.weekday, from: date)

        return FlightSchedule(
            airline: airline,
            flightNumber: "\(carrierIata) \(flightNumber)",
            aircraft: aircraftName,
            aircraftId: equipmentIata ?? "",
            departureTime: depTime,
            arrivalTime: arrTime,
            dateFrom: startOfDay,
            dateTo: startOfDay,
            daysOfWeek: [weekday],
            cabinClasses: classes?.asCabinClass ?? []
        )
    }

    /// Format the wall-clock time from an ISO-with-offset Date as "HH:mm".
    /// The Date already represents the correct UTC instant; we read the
    /// local time component of the airport. Since we lost the original offset
    /// after JSON decoding, we reconstruct it from the leg's stored offset.
    private static func localTimeString(from date: Date, isoZoneFromIATA iata: String) -> String {
        let f = DateFormatter()
        f.dateFormat = "HH:mm"
        // The decoded Date is correct in UTC; the route-explorer API gives
        // local times with offset, so we want to display them as local at
        // the airport. We don't have a per-airport TZ map handy, so use the
        // device locale for now — good enough for HH:mm display since most
        // users browse in their own zone. Connection layovers display via
        // raw Dates, which preserves true elapsed time.
        f.timeZone = .current
        return f.string(from: date)
    }
}

// MARK: - Date decoding for route-explorer ISO timestamps

extension JSONDecoder {
    /// Configures a decoder that parses route-explorer `dateTime` ISO 8601
    /// strings (e.g. "2026-04-28T16:45:00-05:00") into Date.
    static func routeExplorer() -> JSONDecoder {
        let decoder = JSONDecoder()
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        let plainFormatter = ISO8601DateFormatter()
        plainFormatter.formatOptions = [.withInternetDateTime]

        decoder.dateDecodingStrategy = .custom { dec in
            let container = try dec.singleValueContainer()
            let str = try container.decode(String.self)
            if let d = formatter.date(from: str) ?? plainFormatter.date(from: str) {
                return d
            }
            throw DecodingError.dataCorruptedError(
                in: container,
                debugDescription: "Bad date: \(str)"
            )
        }
        return decoder
    }
}
