import Foundation

/// Queries airline APIs for flight load and standby data.
/// Each airline has its own endpoint and response format.
actor AirlineLoadService {

    // MARK: - Properties

    private let session: URLSession
    private let airportDatabase: AirportDatabase?
    private var unitedToken: (hash: String, expiresAt: Date)?

    // MARK: - Init

    init(airportDatabase: AirportDatabase? = nil) {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 15
        session = URLSession(configuration: config)
        self.airportDatabase = airportDatabase
    }

    // MARK: - Public Router

    /// Route to the correct airline based on IATA code.
    ///
    /// `departureTime` is an optional "HH:mm" disambiguator for airlines
    /// (currently JSX / XE) where the flightconnections scraper can return
    /// multiple rows sharing the same flight number but with different
    /// departure times. When provided, the airline-specific fetcher can
    /// match by departure time as a secondary signal.
    func fetchLoad(
        airlineCode: String,
        flightNumber: String,
        date: Date,
        origin: String,
        destination: String,
        departureTime: String? = nil
    ) async -> FlightLoad? {
        let code = airlineCode.uppercased()
        print("[LoadService] Fetching load for \(code) flight \(flightNumber) \(origin)->\(destination)"
            + (departureTime.map { " @ \($0)" } ?? ""))
        switch code {
        case "UA": return await fetchUnitedLoad(flightNumber: flightNumber, date: date, origin: origin)
        case "AA": return await fetchAmericanLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination)
        case "KE": return await fetchKoreanAirLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination)
        case "B6": return await fetchJetBlueStatus(flightNumber: flightNumber, date: date, origin: origin)
        case "AS": return await fetchAlaskaLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination)
        case "EK": return await fetchEmiratesStatus(flightNumber: flightNumber, date: date, origin: origin)
        case "AM": return await fetchAeromexicoLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination)
        case "SY": return await fetchSunCountryLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination)
        case "XE": return await fetchJSXLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination, departureTime: departureTime)
        default:
            print("[LoadService] Unsupported airline: \(code)")
            return nil
        }
    }

    // MARK: - United Airlines

    /// Fetch an anonymous auth token from United, caching until expiry.
    private func getUnitedToken() async -> String? {
        if let cached = unitedToken, cached.expiresAt > Date() {
            return cached.hash
        }

        guard let url = URL(string: "https://www.united.com/api/auth/anonymous-token") else { return nil }

        do {
            var request = URLRequest(url: url)
            Self.applyUnitedBrowserHeaders(to: &request)
            request.setValue("application/json", forHTTPHeaderField: "Accept")

            let (data, response) = try await session.data(for: request)
            let http = response as? HTTPURLResponse
            print("[UA] token HTTP status: \(http?.statusCode ?? -1), \(data.count) bytes")
            guard http?.statusCode == 200 else {
                if let bodyStr = String(data: data, encoding: .utf8) {
                    print("[UA] token body (first 500): \(bodyStr.prefix(500))")
                }
                return nil
            }

            guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
                  let dataObj = json["data"] as? [String: Any],
                  let tokenObj = dataObj["token"] as? [String: Any],
                  let hash = tokenObj["hash"] as? String else {
                print("[UA] token JSON shape unexpected")
                return nil
            }

            // Cache for 25 minutes (tokens typically last 30).
            unitedToken = (hash: hash, expiresAt: Date().addingTimeInterval(25 * 60))
            return hash
        } catch {
            print("[UA] token error: \(error)")
            return nil
        }
    }

    /// Browser-shaped headers for United. The guide notes that plain curl is
    /// blocked by TLS fingerprinting; iOS `URLSession` has a different
    /// fingerprint but United's Akamai rules still sniff `User-Agent` /
    /// `Accept-Language`, so we mirror a desktop browser. Not a guarantee —
    /// if this still fails we may need a WKWebView path like JSX.
    private static func applyUnitedBrowserHeaders(to request: inout URLRequest) {
        request.setValue(
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
                + "(KHTML, like Gecko) Version/17.4 Safari/605.1.15",
            forHTTPHeaderField: "User-Agent"
        )
        request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")
        request.setValue("https://www.united.com/", forHTTPHeaderField: "Referer")
        request.setValue("https://www.united.com", forHTTPHeaderField: "Origin")
    }

    private func fetchUnitedLoad(flightNumber: String, date: Date, origin: String) async -> FlightLoad? {
        guard let token = await getUnitedToken() else { return nil }

        let num = stripAirlinePrefix(flightNumber)
        let dateStr = dayString(from: date, originIATA: origin)

        guard let url = URL(string: "https://www.united.com/api/flightstatus/upgradeListExtended?flightNumber=\(num)&flightDate=\(dateStr)&fromAirportCode=\(origin.uppercased())") else {
            return nil
        }

        do {
            var request = URLRequest(url: url)
            Self.applyUnitedBrowserHeaders(to: &request)
            request.setValue("bearer \(token)", forHTTPHeaderField: "x-authorization-api")
            request.setValue("application/json", forHTTPHeaderField: "Accept")

            let (data, response) = try await session.data(for: request)
            guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
                print("[UA] HTTP status: \((response as? HTTPURLResponse)?.statusCode ?? -1)")
                return nil
            }

            guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
                print("[UA] Failed to parse JSON")
                return nil
            }

            // === LOGGING: Dump top-level keys and structure ===
            print("[UA] ===== RESPONSE FOR UA\(num) =====")
            print("[UA] Top-level keys: \(json.keys.sorted())")

            if let segment = json["segment"] as? [String: Any] {
                print("[UA] segment: \(segment)")
            }

            if let numCabins = json["numberOfCabins"] {
                print("[UA] numberOfCabins: \(numCabins)")
            }

            // Log pbts
            if let pbts = json["pbts"] as? [[String: Any]] {
                print("[UA] pbts count: \(pbts.count)")
                for (i, entry) in pbts.enumerated() {
                    print("[UA]   pbts[\(i)]: \(entry)")
                }
            } else {
                print("[UA] pbts: MISSING or wrong type")
            }

            // Log ALL cabin-level keys (front, middle, rear, etc.)
            for key in json.keys.sorted() {
                if let cabinData = json[key] as? [String: Any],
                   cabinData.keys.contains("standby") || cabinData.keys.contains("cleared") {
                    let clearedList = cabinData["cleared"] as? [[String: Any]] ?? []
                    let standbyListRaw = cabinData["standby"] as? [[String: Any]] ?? []
                    print("[UA] '\(key)' cabin: cleared=\(clearedList.count), standby=\(standbyListRaw.count)")
                    for pax in clearedList.prefix(3) {
                        print("[UA]   cleared: \(pax["passengerName"] ?? "?") seat=\(pax["seatNumber"] ?? "?")")
                    }
                    for pax in standbyListRaw.prefix(3) {
                        print("[UA]   standby: \(pax["passengerName"] ?? "?") seat=\(pax["seatNumber"] ?? "?")")
                    }
                    if clearedList.count > 3 { print("[UA]   ... +\(clearedList.count - 3) more cleared") }
                    if standbyListRaw.count > 3 { print("[UA]   ... +\(standbyListRaw.count - 3) more standby") }
                }
            }

            // Log checkInSummaries if present
            if let checkIn = json["checkInSummaries"] as? [[String: Any]] {
                print("[UA] checkInSummaries count: \(checkIn.count)")
                for entry in checkIn {
                    print("[UA]   checkIn: \(entry)")
                }
            }

            print("[UA] ===== END RESPONSE =====")

            // Parse cabin loads from pbts array
            var cabins: [CabinLoad] = []
            if let pbts = json["pbts"] as? [[String: Any]] {
                for entry in pbts {
                    let cabin = entry["cabin"] as? String ?? "Unknown"
                    let capacity = entry["capacity"] as? Int ?? 0
                    let booked = entry["booked"] as? Int ?? 0
                    let revStandby = entry["revenueStandby"] as? Int ?? 0
                    let sa = entry["sa"] as? Int ?? 0
                    let waitList = entry["waitList"] as? Int ?? 0
                    let jump = entry["jump"] as? Int ?? 0
                    let ps = entry["ps"] as? Int ?? 0

                    print("[UA] Cabin '\(cabin)': cap=\(capacity) booked=\(booked) sa=\(sa) revSB=\(revStandby) waitList=\(waitList) jump=\(jump) ps=\(ps)")

                    cabins.append(CabinLoad(
                        name: cabin,
                        capacity: capacity,
                        booked: booked,
                        revenueStandby: revStandby,
                        nonRevStandby: sa,
                        waitListCount: waitList,
                        jumpSeat: jump
                    ))
                }
            }

            // Parse standby / upgrade lists from ALL cabin sections
            var standbyList: [StandbyPassenger] = []
            var upgradeList: [StandbyPassenger] = []

            // Check all keys that have cleared/standby sub-arrays
            for key in json.keys.sorted() {
                guard let cabinData = json[key] as? [String: Any],
                      cabinData.keys.contains("standby") || cabinData.keys.contains("cleared") else {
                    continue
                }

                let cabinName = key.capitalized

                if let cleared = cabinData["cleared"] as? [[String: Any]] {
                    for pax in cleared {
                        upgradeList.append(StandbyPassenger(
                            order: upgradeList.count + 1,
                            displayName: pax["passengerName"] as? String ?? "",
                            cleared: true,
                            seat: pax["seatNumber"] as? String,
                            listName: cabinName
                        ))
                    }
                }

                if let waiting = cabinData["standby"] as? [[String: Any]] {
                    for pax in waiting {
                        let seat = pax["seatNumber"] as? String
                        let hasSeat = seat != nil && !seat!.isEmpty

                        if hasSeat {
                            // Has a seat = already cleared from upgrade waitlist
                            upgradeList.append(StandbyPassenger(
                                order: upgradeList.count + 1,
                                displayName: pax["passengerName"] as? String ?? "",
                                cleared: true,
                                seat: seat,
                                listName: cabinName
                            ))
                        } else {
                            // No seat = actually waiting on standby
                            standbyList.append(StandbyPassenger(
                                order: standbyList.count + 1,
                                displayName: pax["passengerName"] as? String ?? "",
                                cleared: false,
                                seat: nil,
                                listName: cabinName
                            ))
                        }
                    }
                }
            }

            return FlightLoad(
                airlineCode: "UA",
                flightNumber: "UA\(num)",
                cabins: cabins,
                standbyList: standbyList,
                upgradeList: upgradeList,
                seatAvailability: []
            )
        } catch {
            return nil
        }
    }

    // MARK: - American Airlines

    /// AA gates the waitlist API on User-Agent version. Bump this when
    /// `airlines/com.aa.android_*.apkm` is refreshed — stale versions get
    /// HTTP 403 with `{"alert":{"message":"Please update your version..."}}`.
    private static let aaAppVersion = "2026.14"

    private func fetchAmericanLoad(
        flightNumber: String,
        date: Date,
        origin: String,
        destination: String
    ) async -> FlightLoad? {
        let num = stripAirlinePrefix(flightNumber)
        let dateStr = dayString(from: date, originIATA: origin)

        var components = URLComponents(string: "https://cdn.flyaa.aa.com/api/mobile/loyalty/waitlist/v1.2")
        components?.queryItems = [
            URLQueryItem(name: "carrierCode", value: "AA"),
            URLQueryItem(name: "flightNumber", value: num),
            URLQueryItem(name: "departureDate", value: dateStr),
            URLQueryItem(name: "originAirportCode", value: origin.uppercased()),
            URLQueryItem(name: "destinationAirportCode", value: destination.uppercased())
        ]

        guard let url = components?.url else {
            print("[AA] Invalid URL components")
            return nil
        }

        print("[AA] GET \(url.absoluteString)")

        do {
            var request = URLRequest(url: url)
            request.setValue("Android/\(Self.aaAppVersion) Pixel 7|14|1080|2400|1.0|AmericanAirlines",
                             forHTTPHeaderField: "User-Agent")
            request.setValue("MOBILE", forHTTPHeaderField: "x-clientid")
            request.setValue("application/json", forHTTPHeaderField: "Accept")
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.setValue(UUID().uuidString, forHTTPHeaderField: "Device-ID")
            request.setValue("fs", forHTTPHeaderField: "x-referrer")

            let (data, response) = try await session.data(for: request)
            let http = response as? HTTPURLResponse
            let status = http?.statusCode ?? -1
            print("[AA] HTTP status: \(status), \(data.count) bytes")

            if status != 200 {
                if let bodyStr = String(data: data, encoding: .utf8) {
                    print("[AA] body (first 500): \(bodyStr.prefix(500))")
                    // Server hints when the UA version has aged out — surface it.
                    if status == 403, bodyStr.contains("update your version") {
                        print("[AA] ⚠️ User-Agent version (\(Self.aaAppVersion)) is rejected — bump aaAppVersion to match the latest APK in airlines/")
                    }
                }
                return nil
            }

            guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
                print("[AA] JSON parse failed; body (first 500): \(String(data: data, encoding: .utf8)?.prefix(500) ?? "")")
                return nil
            }
            print("[AA] top-level keys: \(json.keys.sorted())")

            guard let waitListArray = json["waitList"] as? [[String: Any]] else {
                // 200 OK but no `waitList` — typical for AA Eagle 4-digit
                // regional flights (marketed as AA but the mobile waitlist
                // endpoint doesn't track them), or for flights whose waitlist
                // hasn't opened yet (usually opens T-24h before departure).
                print("[AA] No 'waitList' array in response — likely no waitlist open yet for this flight")
                return nil
            }
            print("[AA] waitList entries: \(waitListArray.count)")

            var seatAvailability: [SeatAvailability] = []
            var standbyList: [StandbyPassenger] = []
            var upgradeList: [StandbyPassenger] = []

            for entry in waitListArray {
                let listName = entry["listName"] as? String ?? "Unknown"
                let seatsAvailable = entry["seatsAvailableValue"] as? Int ?? 0
                let semanticColor = entry["seatsAvailableSemanticColor"] as? String ?? "success"

                let label = listName == "First" ? "First Class Upgrades" : "\(listName) Seats"

                seatAvailability.append(SeatAvailability(
                    label: label,
                    available: seatsAvailable,
                    color: SeatAvailabilityColor(rawValue: semanticColor) ?? .success
                ))

                // Parse passengers
                if let passengers = entry["passengers"] as? [[String: Any]] {
                    for pax in passengers {
                        let order = pax["order"] as? Int ?? 0
                        let displayName = pax["displayName"] as? String ?? ""
                        let cleared = pax["cleared"] as? Bool ?? false
                        let seat = pax["seat"] as? String

                        let passenger = StandbyPassenger(
                            order: order,
                            displayName: displayName,
                            cleared: cleared,
                            seat: seat,
                            listName: listName
                        )

                        if listName.lowercased() == "standby" {
                            standbyList.append(passenger)
                        } else {
                            upgradeList.append(passenger)
                        }
                    }
                }
            }

            print("[AA] parsed seatAvailability=\(seatAvailability.count) standby=\(standbyList.count) upgrade=\(upgradeList.count)")
            return FlightLoad(
                airlineCode: "AA",
                flightNumber: "AA\(num)",
                cabins: [],
                standbyList: standbyList,
                upgradeList: upgradeList,
                seatAvailability: seatAvailability
            )
        } catch {
            print("[AA] error: \(error)")
            return nil
        }
    }

    // MARK: - Korean Air

    private func fetchKoreanAirLoad(
        flightNumber: String,
        date: Date,
        origin: String,
        destination: String
    ) async -> FlightLoad? {
        guard let url = URL(string: "https://www.koreanair.com/api/et/ibeSupport/flightSeatCount") else {
            return nil
        }

        let num = stripAirlinePrefix(flightNumber)
        let dateStr = compactDayString(from: date, originIATA: origin)

        let body: [String: String] = [
            "carrierCode": "KE",
            "flightNumber": num,
            "departureAirport": origin.uppercased(),
            "arrivalAirport": destination.uppercased(),
            "departureDate": dateStr
        ]

        print("[KE] POST \(url) body: \(body)")

        do {
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.setValue("application/json", forHTTPHeaderField: "Accept")
            request.setValue("pc", forHTTPHeaderField: "channel")
            request.httpBody = try JSONSerialization.data(withJSONObject: body)

            let (data, response) = try await session.data(for: request)
            let http = response as? HTTPURLResponse
            print("[KE] HTTP status: \(http?.statusCode ?? -1), \(data.count) bytes")

            if let bodyStr = String(data: data, encoding: .utf8) {
                print("[KE] body (first 1200): \(bodyStr.prefix(1200))")
            }

            guard http?.statusCode == 200 else { return nil }
            guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
                print("[KE] JSON parse failed")
                return nil
            }

            // Guide doesn't document the response shape; try a few likely keys
            // and fall back to scanning any top-level container for Int values
            // named like "seatCount". Log what we see so the real shape is
            // visible in the Xcode console on first run.
            print("[KE] top-level keys: \(json.keys.sorted())")

            let seatCount = (json["seatCount"] as? Int)
                ?? (json["availableSeatCount"] as? Int)
                ?? (json["totalSeatCount"] as? Int)
                ?? ((json["data"] as? [String: Any])?["seatCount"] as? Int)
                ?? 0

            print("[KE] parsed seatCount=\(seatCount)")

            // KE doesn't expose capacity/booked — report what we know as
            // non-revenue-standby availability in an Economy-labeled cabin
            // (matches how we presented this previously).
            let cabin = CabinLoad(
                name: "Economy",
                capacity: 0,
                booked: 0,
                revenueStandby: 0,
                nonRevStandby: seatCount
            )

            return FlightLoad(
                airlineCode: "KE",
                flightNumber: "KE\(num)",
                cabins: [cabin],
                standbyList: [],
                upgradeList: [],
                seatAvailability: []
            )
        } catch {
            print("[KE] error: \(error)")
            return nil
        }
    }

    // MARK: - JetBlue

    private func fetchJetBlueStatus(flightNumber: String, date: Date, origin: String) async -> FlightLoad? {
        let num = stripAirlinePrefix(flightNumber)
        let dateStr = dayString(from: date, originIATA: origin)

        guard let url = URL(string: "https://az-api.jetblue.com/flight-status/get-by-number?number=\(num)&date=\(dateStr)") else {
            print("[B6] Invalid URL")
            return nil
        }

        print("[B6] GET \(url)")

        do {
            var request = URLRequest(url: url)
            request.setValue("49fc015f1ba44abf892d2b8961612378", forHTTPHeaderField: "apikey")
            request.setValue("application/json", forHTTPHeaderField: "Accept")

            let (data, response) = try await session.data(for: request)
            let http = response as? HTTPURLResponse
            print("[B6] HTTP status: \(http?.statusCode ?? -1)")

            if let bodyStr = String(data: data, encoding: .utf8) {
                print("[B6] Response: \(bodyStr.prefix(800))")
            }

            guard http?.statusCode == 200 else { return nil }

            guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
                  let flights = json["flights"] as? [[String: Any]],
                  let flight = flights.first,
                  let legs = flight["legs"] as? [[String: Any]],
                  let leg = legs.first else {
                print("[B6] Failed to parse flight data")
                return nil
            }

            let flightNo = leg["flightNo"] as? String ?? num
            let status = leg["flightStatus"] as? String ?? "Unknown"
            print("[B6] Flight B6\(flightNo) status: \(status)")

            // JetBlue flight status only — no seat/standby data without check-in session
            return FlightLoad(
                airlineCode: "B6",
                flightNumber: "B6\(flightNo)",
                cabins: [],
                standbyList: [],
                upgradeList: [],
                seatAvailability: []
            )
        } catch {
            print("[B6] Error: \(error)")
            return nil
        }
    }

    // MARK: - Alaska Airlines

    /// Static APIM key extracted from the Alaska mobile app, per
    /// AIRLINE_INTEGRATION_GUIDE.md. Accepts `confirmationCode: null` on the
    /// `/seats/waitlist` POST, so we get standby + upgrade lists without a PNR.
    private static let alaskaAPIMKey = "de1d0ff837444468a5ea868945aab738"

    private func fetchAlaskaLoad(
        flightNumber: String,
        date: Date,
        origin: String,
        destination: String
    ) async -> FlightLoad? {
        let num = stripAirlinePrefix(flightNumber)
        let dateStr = dayString(from: date, originIATA: origin)
        let upperOrigin = origin.uppercased()
        let upperDestination = destination.uppercased()

        print("[AS] Fetching Alaska load for AS\(num) \(upperOrigin)->\(upperDestination) on \(dateStr)")

        // Run seatmap + waitlist in parallel; either may fail independently.
        async let seatmap = fetchAlaskaSeatmap(
            flightNumber: num,
            origin: upperOrigin,
            destination: upperDestination,
            date: dateStr
        )
        async let waitlist = fetchAlaskaWaitlist(
            flightNumber: num,
            origin: upperOrigin,
            date: dateStr
        )

        let cabinsFromMap = await seatmap
        let waitlistResult = await waitlist
        let cabinsFromWaitlist = waitlistResult.0
        let standbyList = waitlistResult.1
        let upgradeList = waitlistResult.2

        // Prefer waitlist's `Authorized` cabin capacity (real FlightLoad numbers)
        // over seatmap's `AvailableSeats` (count of open seats only).
        let cabins = !cabinsFromWaitlist.isEmpty ? cabinsFromWaitlist : cabinsFromMap

        if cabins.isEmpty && standbyList.isEmpty && upgradeList.isEmpty {
            print("[AS] No data returned from seatmap or waitlist endpoints")
            return nil
        }

        return FlightLoad(
            airlineCode: "AS",
            flightNumber: "AS\(num)",
            cabins: cabins,
            standbyList: standbyList,
            upgradeList: upgradeList,
            seatAvailability: []
        )
    }

    /// GET /1/guestservices/customermobile/viewseatmap/seatmap
    /// Returns per-seat status + `AvailableSeats` count per cabin section.
    private func fetchAlaskaSeatmap(
        flightNumber: String,
        origin: String,
        destination: String,
        date: String
    ) async -> [CabinLoad] {
        var components = URLComponents(string: "https://apis.alaskaair.com/1/guestservices/customermobile/viewseatmap/seatmap")
        components?.queryItems = [
            URLQueryItem(name: "flightnumber", value: flightNumber),
            URLQueryItem(name: "departureairport", value: origin),
            URLQueryItem(name: "arrivalairport", value: destination),
            URLQueryItem(name: "departuredate", value: date)
        ]
        guard let url = components?.url else {
            print("[AS] seatmap: invalid URL")
            return []
        }

        print("[AS] GET \(url)")

        do {
            var request = URLRequest(url: url)
            request.setValue(Self.alaskaAPIMKey, forHTTPHeaderField: "Ocp-Apim-Subscription-Key")
            request.setValue("application/json", forHTTPHeaderField: "Accept")

            let (data, response) = try await session.data(for: request)
            let http = response as? HTTPURLResponse
            print("[AS] seatmap HTTP status: \(http?.statusCode ?? -1), \(data.count) bytes")

            if let bodyStr = String(data: data, encoding: .utf8) {
                print("[AS] seatmap body (first 1200): \(bodyStr.prefix(1200))")
            }

            guard http?.statusCode == 200 else { return [] }
            guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [] }

            // Per the guide, the response carries `AvailableSeats` per cabin section.
            // The exact wrapper differs by route; we walk the top level looking for
            // anything that has {AvailableSeats, ...} or a list of cabin sections.
            var cabins: [CabinLoad] = []

            // Common shapes: { Cabins: [{ Name, AvailableSeats, ... }] } or
            // { Sections: [...] } or the cabin fields inline at root.
            if let cabinArr = (json["Cabins"] as? [[String: Any]])
                ?? (json["cabins"] as? [[String: Any]])
                ?? (json["Sections"] as? [[String: Any]]) {
                for entry in cabinArr {
                    let name = (entry["Name"] as? String)
                        ?? (entry["CabinName"] as? String)
                        ?? (entry["Description"] as? String)
                        ?? "Cabin"
                    let available = (entry["AvailableSeats"] as? Int)
                        ?? (entry["availableSeats"] as? Int)
                        ?? 0
                    // Capacity is best-effort; fall back to available so
                    // loadFactor stays sane when we only have open seats.
                    let capacity = (entry["TotalSeats"] as? Int)
                        ?? (entry["Capacity"] as? Int)
                        ?? available
                    let booked = max(0, capacity - available)
                    cabins.append(CabinLoad(
                        name: name,
                        capacity: capacity,
                        booked: booked,
                        revenueStandby: 0,
                        nonRevStandby: 0
                    ))
                }
            }

            print("[AS] seatmap parsed \(cabins.count) cabins")
            return cabins
        } catch {
            print("[AS] seatmap error: \(error)")
            return []
        }
    }

    /// POST /1/guestservices/customermobile/seats/waitlist
    /// Returns StandbyList + UpgradeList with FlightLoad.Authorized (capacity)
    /// and passenger names. `confirmationCode: null` is accepted.
    private func fetchAlaskaWaitlist(
        flightNumber: String,
        origin: String,
        date: String
    ) async -> ([CabinLoad], [StandbyPassenger], [StandbyPassenger]) {
        guard let url = URL(string: "https://apis.alaskaair.com/1/guestservices/customermobile/seats/waitlist") else {
            print("[AS] waitlist: invalid URL")
            return ([], [], [])
        }

        let body: [String: Any?] = [
            "marketedByAirlineCode": "AS",
            "departureAirportCode": origin,
            "departureLocalDate": date,
            "flightNumber": flightNumber,
            "confirmationCode": NSNull()
        ]

        print("[AS] POST \(url) body flight=\(flightNumber) origin=\(origin) date=\(date)")

        do {
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.setValue(Self.alaskaAPIMKey, forHTTPHeaderField: "Ocp-Apim-Subscription-Key")
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.setValue("application/json", forHTTPHeaderField: "Accept")
            request.httpBody = try JSONSerialization.data(withJSONObject: body.compactMapValues { $0 as Any? })

            let (data, response) = try await session.data(for: request)
            let http = response as? HTTPURLResponse
            print("[AS] waitlist HTTP status: \(http?.statusCode ?? -1), \(data.count) bytes")

            if let bodyStr = String(data: data, encoding: .utf8) {
                print("[AS] waitlist body (first 1500): \(bodyStr.prefix(1500))")
            }

            guard http?.statusCode == 200 else { return ([], [], []) }
            guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
                return ([], [], [])
            }

            let (mainCap, mainPax) = parseAlaskaWaitlistSection(json["StandbyList"], listLabel: "Standby")
            let (upgradeCap, upgradePax) = parseAlaskaWaitlistSection(json["UpgradeList"], listLabel: "Upgrade")

            var cabins: [CabinLoad] = []
            if mainCap > 0 {
                cabins.append(CabinLoad(
                    name: "Main",
                    capacity: mainCap,
                    booked: 0,
                    revenueStandby: 0,
                    nonRevStandby: 0
                ))
            }
            if upgradeCap > 0 {
                cabins.append(CabinLoad(
                    name: "First",
                    capacity: upgradeCap,
                    booked: 0,
                    revenueStandby: 0,
                    nonRevStandby: 0
                ))
            }

            return (cabins, mainPax, upgradePax)
        } catch {
            print("[AS] waitlist error: \(error)")
            return ([], [], [])
        }
    }

    /// Parse one of the waitlist sections (StandbyList / UpgradeList).
    /// Returns (cabinCapacity, passengers).
    private func parseAlaskaWaitlistSection(_ raw: Any?, listLabel: String) -> (Int, [StandbyPassenger]) {
        guard let section = raw as? [String: Any] else { return (0, []) }

        let flightLoad = section["FlightLoad"] as? [String: Any] ?? [:]
        let authorized = (flightLoad["Authorized"] as? Int) ?? 0

        var passengers: [StandbyPassenger] = []
        if let paxArr = section["Passengers"] as? [[String: Any]] {
            for pax in paxArr {
                let name = (pax["DisplayName"] as? String) ?? ""
                let position = (pax["Position"] as? Int) ?? (passengers.count + 1)
                let seat = pax["Seat"] as? String
                let upgraded = (pax["UpgradedToPC"] as? Bool) ?? false
                let cleared = upgraded || (seat != nil && !(seat?.isEmpty ?? true))
                passengers.append(StandbyPassenger(
                    order: position,
                    displayName: name,
                    cleared: cleared,
                    seat: seat,
                    listName: listLabel
                ))
            }
        }

        print("[AS] \(listLabel) section: authorized=\(authorized), passengers=\(passengers.count)")
        return (authorized, passengers)
    }

    // MARK: - Emirates

    private func fetchEmiratesStatus(flightNumber: String, date: Date, origin: String) async -> FlightLoad? {
        let num = stripAirlinePrefix(flightNumber)
        let dateStr = dayString(from: date, originIATA: origin)

        // Guide sample uses `flight=221` unpadded; padding was an unverified
        // guess and is now removed to match the documented call exactly.
        guard let url = URL(string: "https://www.emirates.com/service/flight-status?departureDate=\(dateStr)&flight=\(num)") else {
            print("[EK] Invalid URL")
            return nil
        }

        print("[EK] GET \(url)")

        do {
            var request = URLRequest(url: url)
            request.setValue("application/json", forHTTPHeaderField: "Accept")

            let (data, response) = try await session.data(for: request)
            let http = response as? HTTPURLResponse
            print("[EK] HTTP status: \(http?.statusCode ?? -1)")

            if let bodyStr = String(data: data, encoding: .utf8) {
                print("[EK] Response: \(bodyStr.prefix(800))")
            }

            guard http?.statusCode == 200 else { return nil }

            guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
                  let results = json["results"] as? [[String: Any]],
                  let flight = results.first else {
                print("[EK] Failed to parse flight data")
                return nil
            }

            let flightNo = flight["flightNumber"] as? String ?? num
            print("[EK] Flight EK\(flightNo) found")

            // Emirates flight status only — seat/load data requires PNR
            return FlightLoad(
                airlineCode: "EK",
                flightNumber: "EK\(flightNo)",
                cabins: [],
                standbyList: [],
                upgradeList: [],
                seatAvailability: []
            )
        } catch {
            print("[EK] Error: \(error)")
            return nil
        }
    }

    // MARK: - Aeromexico

    /// Aeromexico exposes a Sabre `GetPassengerListRQ` proxy on a public AWS
    /// API Gateway used by their consumer app's flight-status widget. The
    /// endpoint requires no API key — just a `channel: web` / `flow: CHECKIN`
    /// header pair (constants extracted from the AM Android APK).
    ///
    /// Response includes:
    ///   - `cabinInfoList[].authorized` (capacity) + `.available` (open seats)
    ///   - `passengers[]` with full PII, priority, `isStaff` flag, positions
    ///
    /// Snapshot persists at least T-1d through T+2d; outside that the gateway
    /// answers `FLIGHT NOT INITIALIZED` or `NONE LISTED`.
    private func fetchAeromexicoLoad(
        flightNumber: String,
        date: Date,
        origin: String,
        destination: String
    ) async -> FlightLoad? {
        let num = stripAirlinePrefix(flightNumber)
        // Endpoint validates flight code against ^[0-9]{4}$.
        let padded = String(format: "%04d", Int(num) ?? 0)
        let dateStr = dayString(from: date, originIATA: origin)

        async let standbyResp = fetchAeromexicoList(
            endpoint: "passengerliststandby",
            flightCode: padded,
            origin: origin,
            departureDate: dateStr
        )
        async let upgradeResp = fetchAeromexicoList(
            endpoint: "passengerlistupgrade",
            flightCode: padded,
            origin: origin,
            departureDate: dateStr
        )

        let sb = await standbyResp
        let up = await upgradeResp

        // If both calls failed entirely we have nothing.
        guard sb != nil || up != nil else {
            print("[AM] Both standby and upgrade calls returned nil")
            return nil
        }

        // Cabin info: same per leg, either response carries it.
        var cabins: [CabinLoad] = []
        if let cabinList = sb?["cabinInfoList"] as? [[String: Any]] {
            cabins = Self.parseAeromexicoCabins(cabinList)
        } else if let cabinList = up?["cabinInfoList"] as? [[String: Any]] {
            cabins = Self.parseAeromexicoCabins(cabinList)
        }

        let standbyList = Self.parseAeromexicoPassengers(
            sb?["passengers"] as? [[String: Any]] ?? [],
            listName: "Standby"
        )
        let upgradeList = Self.parseAeromexicoPassengers(
            up?["passengers"] as? [[String: Any]] ?? [],
            listName: "Upgrade"
        )

        let sbTotal = sb?["totalListed"] as? Int ?? 0
        let upTotal = up?["totalListed"] as? Int ?? 0
        print("[AM] parsed cabins=\(cabins.count) standby=\(standbyList.count)/\(sbTotal) upgrade=\(upgradeList.count)/\(upTotal)")

        // Surface "no data yet" cleanly: if every response was NONE LISTED and
        // we got nothing back, return nil so the detail view shows the
        // "Load data not available" state rather than an empty card.
        if cabins.isEmpty && standbyList.isEmpty && upgradeList.isEmpty {
            print("[AM] No usable data in response (snapshot likely outside T-1d / T+2d window)")
            return nil
        }

        return FlightLoad(
            airlineCode: "AM",
            flightNumber: "AM\(num)",
            cabins: cabins,
            standbyList: standbyList,
            upgradeList: upgradeList,
            seatAvailability: []
        )
    }

    /// Single GET against the AM passenger-list gateway. Returns the parsed
    /// JSON dict on success (with or without populated lists), or nil if the
    /// request failed at the transport layer.
    private func fetchAeromexicoList(
        endpoint: String,
        flightCode: String,
        origin: String,
        departureDate: String
    ) async -> [String: Any]? {
        var components = URLComponents(string: "https://lw18yj0mhb.execute-api.us-east-1.amazonaws.com/rb/\(endpoint)")
        components?.queryItems = [
            URLQueryItem(name: "departureAirport", value: origin.uppercased()),
            URLQueryItem(name: "code", value: flightCode),
            URLQueryItem(name: "departureDate", value: departureDate),
            URLQueryItem(name: "operatingCarrier", value: "AM"),
            URLQueryItem(name: "operatingFlightCode", value: flightCode)
        ]
        guard let url = components?.url else {
            print("[AM] Invalid URL for \(endpoint)")
            return nil
        }
        print("[AM] GET \(url.absoluteString)")

        do {
            var request = URLRequest(url: url)
            request.setValue("web", forHTTPHeaderField: "channel")
            request.setValue("CHECKIN", forHTTPHeaderField: "flow")
            request.setValue(UUID().uuidString, forHTTPHeaderField: "x-transaction-id")
            request.setValue("application/json", forHTTPHeaderField: "Accept")

            let (data, response) = try await session.data(for: request)
            let status = (response as? HTTPURLResponse)?.statusCode ?? -1
            print("[AM] \(endpoint) HTTP \(status), \(data.count) bytes")
            guard status == 200 else {
                if let body = String(data: data, encoding: .utf8) {
                    print("[AM] \(endpoint) non-200 body: \(body.prefix(300))")
                }
                return nil
            }
            guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
                print("[AM] \(endpoint) JSON parse failed")
                return nil
            }
            // Log the warning surface so future failures are diagnosable.
            if let warnings = json["warnings"] as? [[String: Any]], !warnings.isEmpty {
                let msgs = warnings.compactMap { $0["errorMessage"] as? String }.joined(separator: " | ")
                print("[AM] \(endpoint) warnings: \(msgs)")
            }
            return json
        } catch {
            print("[AM] \(endpoint) error: \(error)")
            return nil
        }
    }

    private static func parseAeromexicoCabins(_ raw: [[String: Any]]) -> [CabinLoad] {
        raw.compactMap { entry in
            let cabinCode = entry["cabin"] as? String ?? "?"
            let authorized = entry["authorized"] as? Int ?? 0
            let available = entry["available"] as? Int ?? 0
            // Skip placeholder rows with no capacity at all.
            guard authorized > 0 || available > 0 else { return nil }
            let booked = max(0, authorized - available)
            return CabinLoad(
                name: aeromexicoCabinName(code: cabinCode),
                capacity: authorized,
                booked: booked,
                revenueStandby: 0,
                nonRevStandby: 0
            )
        }
    }

    /// Map AM's single-letter cabin codes to user-readable names. AM uses
    /// `Y` for economy, `C` (Clase Premier) for business, and `P` for
    /// Premier One (their first/long-haul biz). Anything unknown falls
    /// through with the raw code.
    private static func aeromexicoCabinName(code: String) -> String {
        switch code.uppercased() {
        case "Y": return "Economy"
        case "C": return "Clase Premier"
        case "P": return "Premier One"
        case "F": return "First"
        default:  return code
        }
    }

    private static func parseAeromexicoPassengers(
        _ raw: [[String: Any]],
        listName: String
    ) -> [StandbyPassenger] {
        raw.enumerated().compactMap { (index, entry) in
            let first = entry["firstName"] as? String ?? ""
            let last = entry["lastName"] as? String ?? ""
            // AM lists names in full, redact for display the way AA does
            // (last name + first initial). If either piece is missing,
            // fall back gracefully.
            let display: String
            if !last.isEmpty, !first.isEmpty {
                display = "\(last), \(first.prefix(1))"
            } else {
                display = (last.isEmpty ? first : last)
            }

            let position = (entry["newPosition"] as? Int)
                ?? (entry["originalPosition"] as? Int)
                ?? (index + 1)

            let cleared = (entry["boardingPassFlag"] as? Bool ?? false)
                || (entry["boardStatus"] as? Bool ?? false)
                || ((entry["seat"] as? String).map { !$0.isEmpty } ?? false)

            return StandbyPassenger(
                order: position,
                displayName: display,
                cleared: cleared,
                seat: entry["seat"] as? String,
                listName: listName
            )
        }
    }

    // MARK: - Sun Country

    /// Sun Country runs on Navitaire (same PSS as JSX). Their public booking
    /// availability search returns full per-flight inventory data including
    /// `sold` (booked passenger count) and `capacity` per leg — better than
    /// AA, which only gives seat-availability counts. No standby program
    /// exposed via this endpoint (SY is single-class), so we return cabin
    /// load only.
    ///
    /// Imperva WAF in front of `syprod-api.suncountry.com` blocks bare
    /// curl. Gated on User-Agent / Referer / Origin headers (same pattern
    /// as American). Browser-shaped headers pass cleanly.
    ///
    /// Endpoint: `POST /api/nsk/v4/availability/search/simple`
    /// Auth: Azure APIM key + a long-lived dotREZ JWT (both extracted from
    /// network traffic of suncountry.com; neither is a user session token).
    private static let sunCountryAPIMKey = "bc7f707786c44a56859c396102f6cd21"

    /// dotREZ JWT used as the `Authorization` header. Issued by "dotREZ
    /// API" with `sub: DOTREZ` — a static API client identity, not a user
    /// token. If this stops working, capture a fresh one from
    /// suncountry.com's PUT /api/nsk/v1/token request.
    private static let sunCountryJWT =
        "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJET1RSRVoiLCJqdGkiOiI0ZWQyNjQ0Ny0zOTU4LWQ1YjQtZTkxNi0xZDM4YWFiNTQ0ZTMiLCJpc3MiOiJkb3RSRVogQVBJIn0.W_zpG_6nZbD37S7hsWgahYG9Dc1gwgG_8s0KA3V72Qg"

    private func fetchSunCountryLoad(
        flightNumber: String,
        date: Date,
        origin: String,
        destination: String
    ) async -> FlightLoad? {
        let num = stripAirlinePrefix(flightNumber)
        let dateStr = dayString(from: date, originIATA: origin)

        guard let url = URL(string: "https://syprod-api.suncountry.com/api/nsk/v4/availability/search/simple") else {
            print("[SY] Invalid URL")
            return nil
        }

        let body: [String: Any] = [
            "Origin": origin.uppercased(),
            "Destination": destination.uppercased(),
            "BeginDate": dateStr,
            "EndDate": dateStr,
            "Passengers": ["Types": [["Type": "ADT", "Count": 1]]],
            "Currency": "USD"
        ]

        print("[SY] POST \(url.absoluteString) for SY\(num) \(origin)→\(destination) on \(dateStr)")

        do {
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            Self.applySunCountryBrowserHeaders(to: &request)
            request.setValue(Self.sunCountryAPIMKey, forHTTPHeaderField: "Ocp-Apim-Subscription-Key")
            request.setValue(Self.sunCountryJWT, forHTTPHeaderField: "Authorization")
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.setValue("application/json", forHTTPHeaderField: "Accept")
            request.httpBody = try JSONSerialization.data(withJSONObject: body)

            let (data, response) = try await session.data(for: request)
            let status = (response as? HTTPURLResponse)?.statusCode ?? -1
            print("[SY] HTTP status: \(status), \(data.count) bytes")

            if status != 200 {
                if let bodyStr = String(data: data, encoding: .utf8) {
                    print("[SY] body (first 500): \(bodyStr.prefix(500))")
                    if status == 403, bodyStr.contains("Incapsula") || bodyStr.contains("Imperva") {
                        print("[SY] ⚠️ Imperva WAF rejection — check the User-Agent / Referer / Origin headers in applySunCountryBrowserHeaders")
                    }
                }
                return nil
            }

            guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
                  let dataObj = json["data"] as? [String: Any],
                  let results = dataObj["results"] as? [[String: Any]] else {
                print("[SY] Response shape unexpected — top-level keys: \((try? JSONSerialization.jsonObject(with: data) as? [String: Any])?.keys.sorted() ?? [])")
                return nil
            }

            // Walk results → trips → journeysAvailableByMarket → match by
            // flight number, then read sold/capacity from the leg.
            let key = "\(origin.uppercased())|\(destination.uppercased())"
            var match: [String: Any]?
            outer: for result in results {
                for trip in (result["trips"] as? [[String: Any]]) ?? [] {
                    let market = trip["journeysAvailableByMarket"] as? [String: Any]
                    for journey in (market?[key] as? [[String: Any]]) ?? [] {
                        let segments = journey["segments"] as? [[String: Any]] ?? []
                        let first = segments.first
                        let flightId = ((first?["identifier"] as? [String: Any])?["identifier"] as? String) ?? ""
                        if flightId == num {
                            match = journey
                            break outer
                        }
                    }
                }
            }

            guard let journey = match,
                  let segments = journey["segments"] as? [[String: Any]],
                  let legs = (segments.first?["legs"] as? [[String: Any]]),
                  let legInfo = legs.first?["legInfo"] as? [String: Any] else {
                print("[SY] No matching flight \(num) in response for \(origin)-\(destination)")
                return nil
            }

            let capacity = (legInfo["capacity"] as? Int) ?? (legInfo["adjustedCapacity"] as? Int) ?? 0
            let sold = legInfo["sold"] as? Int ?? 0
            let equipment = legInfo["equipmentType"] as? String ?? ""

            print("[SY] Found SY\(num): capacity=\(capacity) sold=\(sold) equipment=\(equipment) load=\(capacity > 0 ? Double(sold)/Double(capacity) : 0)")

            if capacity <= 0 {
                print("[SY] Capacity was 0; treating as no data")
                return nil
            }

            let cabin = CabinLoad(
                name: "Economy",
                capacity: capacity,
                booked: sold,
                revenueStandby: 0,
                nonRevStandby: 0
            )

            return FlightLoad(
                airlineCode: "SY",
                flightNumber: "SY\(num)",
                cabins: [cabin],
                standbyList: [],
                upgradeList: [],
                seatAvailability: []
            )
        } catch {
            print("[SY] error: \(error)")
            return nil
        }
    }

    /// Browser-shaped headers so Imperva lets the request through. The
    /// API host is gated on User-Agent + Referer + Origin; bare curl
    /// (or default URLSession) gets 403 with an Incapsula page.
    private static func applySunCountryBrowserHeaders(to request: inout URLRequest) {
        request.setValue(
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
                + "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
            forHTTPHeaderField: "User-Agent"
        )
        request.setValue("https://www.suncountry.com/", forHTTPHeaderField: "Referer")
        request.setValue("https://www.suncountry.com", forHTTPHeaderField: "Origin")
        request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")
    }

    // MARK: - JSX (JetSuiteX)

    private func fetchJSXLoad(
        flightNumber: String,
        date: Date,
        origin: String,
        destination: String,
        departureTime: String?
    ) async -> FlightLoad? {
        let dateStr = dayString(from: date, originIATA: origin)
        let num = stripAirlinePrefix(flightNumber)
        let upperOrigin = origin.uppercased()
        let upperDestination = destination.uppercased()

        print("[XE] Fetching JSX for XE\(num) \(upperOrigin)->\(upperDestination) on \(dateStr)"
            + (departureTime.map { " @ \($0)" } ?? ""))

        let fetcher = await JSXWebViewFetcher()
        let result = await fetcher.fetchAvailability(
            origin: upperOrigin,
            destination: upperDestination,
            date: dateStr
        )

        if let error = result.error {
            print("[XE] JSX flow failed: \(error)")
            return nil
        }

        print("[XE] JSX returned \(result.flights.count) unique flights for \(upperOrigin)|\(upperDestination):")
        for f in result.flights {
            let low = f.lowestFareTotal.map { "$\(Int($0))" } ?? "n/a"
            let depHHmm = jsxLocalTimeHHmm(f.departureLocal)
            print("[XE]   - \(f.flightNumber)  \(f.departureLocal) (\(depHHmm))  seats=\(f.totalAvailable)  low=\(low)")
        }

        let capacity = 30 // JSX ERJ-145 approximate capacity

        // Preferred path: per-flight data from /search/simple.
        if !result.flights.isEmpty {
            let targetDigits = num.filter(\.isNumber)

            // Primary: match by digits-only flight number. This works for
            // genuinely-distinct flight numbers (XE280 vs XE292 vs XE290).
            let byNumber = result.flights.filter {
                $0.flightNumber.filter(\.isNumber) == targetDigits
            }

            // If the number alone identifies a unique flight, use it.
            if byNumber.count == 1 {
                let flight = byNumber[0]
                print("[XE] MATCH by flight number: \(flight.flightNumber) seats=\(flight.totalAvailable)")
                return makeJSXFlightLoad(flight: flight, capacity: capacity)
            }

            // Secondary: if multiple flights share the same number (which
            // happens when the flightconnections scraper collapses distinct
            // departures under one flightnumber row), or if the number
            // matches nothing, use the caller's departureTime as the tie
            // breaker. JSXFlight.departureLocal is "YYYY-MM-DDTHH:mm:ss";
            // FlightSchedule.departureTime is "HH:mm".
            if let wantHHmm = departureTime, !wantHHmm.isEmpty {
                let pool = byNumber.isEmpty ? result.flights : byNumber
                if let flight = pool.first(where: { jsxLocalTimeHHmm($0.departureLocal) == wantHHmm }) {
                    print("[XE] MATCH by departureTime \(wantHHmm): \(flight.flightNumber) seats=\(flight.totalAvailable)"
                        + (byNumber.isEmpty ? " (ignored flight number)" : " (tie-break among \(byNumber.count) same-number flights)"))
                    return makeJSXFlightLoad(flight: flight, capacity: capacity)
                }
                print("[XE] departureTime \(wantHHmm) did not match any of \(result.flights.count) flights")
            }

            // Last resort in the primary path: if byNumber has anything,
            // take the first. Report explicitly so we can tell this apart
            // from a clean match in the logs.
            if let flight = byNumber.first {
                print("[XE] MATCH (ambiguous first-of-\(byNumber.count)): \(flight.flightNumber) seats=\(flight.totalAvailable)")
                return makeJSXFlightLoad(flight: flight, capacity: capacity)
            }

            print("[XE] Flight XE\(num) not present in \(result.flights.count) results (no number or time match)")
            return nil
        }

        // Graceful degradation: /search/simple was unreachable but we have
        // the /lowfare/estimate day-total, which gives us at least
        // "how many seats are available somewhere on the route today".
        // We can't attribute that to a specific flight, so we show it as
        // a route-level cabin so the UI isn't empty.
        if let lowFare = result.lowFareFallback {
            print("[XE] Falling back to low-fare estimate: \(lowFare.available) seats available on \(lowFare.date)")
            let booked = max(0, capacity - lowFare.available)
            return FlightLoad(
                airlineCode: "XE",
                flightNumber: "XE\(num)",
                cabins: [
                    CabinLoad(
                        name: "Route (day total)",
                        capacity: capacity,
                        booked: booked,
                        revenueStandby: 0,
                        nonRevStandby: 0
                    )
                ],
                standbyList: [],
                upgradeList: [],
                seatAvailability: []
            )
        }

        print("[XE] No per-flight data and no low-fare fallback; giving up")
        return nil
    }

    // MARK: - JSX helpers

    /// Build a single-cabin `FlightLoad` from a `JSXFlight`. Factored out so
    /// all the match paths in `fetchJSXLoad` return the same shape.
    private func makeJSXFlightLoad(flight: JSXFlight, capacity: Int) -> FlightLoad {
        let booked = max(0, capacity - flight.totalAvailable)
        return FlightLoad(
            airlineCode: "XE",
            flightNumber: flight.flightNumber,
            cabins: [
                CabinLoad(
                    name: "Cabin",
                    capacity: capacity,
                    booked: booked,
                    revenueStandby: 0,
                    nonRevStandby: 0
                )
            ],
            standbyList: [],
            upgradeList: [],
            seatAvailability: []
        )
    }

    /// Extract "HH:mm" from a JSX local time string like
    /// "2026-04-15T17:35:00". Returns "" on malformed input.
    private func jsxLocalTimeHHmm(_ local: String) -> String {
        // Expect "YYYY-MM-DDTHH:mm:ss" — the HH:mm starts at index 11.
        guard let tIdx = local.firstIndex(of: "T") else { return "" }
        let after = local.index(after: tIdx)
        guard local.distance(from: after, to: local.endIndex) >= 5 else { return "" }
        let end = local.index(after, offsetBy: 5)
        return String(local[after..<end])
    }

    // MARK: - Helpers

    /// Strip any airline prefix from a flight number string.
    /// "AA 2238" -> "2238", "UA2238" -> "2238", "2238" -> "2238"
    private func stripAirlinePrefix(_ raw: String) -> String {
        let trimmed = raw.trimmingCharacters(in: .whitespaces)

        // Drop leading letters and any space to get just the numeric portion
        var start = trimmed.startIndex
        while start < trimmed.endIndex && (trimmed[start].isLetter || trimmed[start] == " ") {
            start = trimmed.index(after: start)
        }

        let result = String(trimmed[start...])
        return result.isEmpty ? trimmed : result
    }

    /// "yyyy-MM-dd" formatter for United and American.
    /// NOTE: this is UTC-pinned and will cross the day boundary for users in
    /// non-UTC timezones. Prefer `dayString(from:originIATA:)` which resolves
    /// the departure airport's approximate local timezone.
    private static let dashDateFormatter: DateFormatter = {
        let f = DateFormatter()
        f.dateFormat = "yyyy-MM-dd"
        f.locale = Locale(identifier: "en_US_POSIX")
        f.timeZone = TimeZone(identifier: "UTC")
        return f
    }()

    /// Format a `Date` as "yyyy-MM-dd" in the *departure airport's* local
    /// timezone. Airlines (JSX especially) interpret the day string as the
    /// calendar day at the departure airport — not UTC, not the user's locale.
    ///
    /// Timezone resolution strategy:
    ///   1. Look up the airport's longitude in the bundled airports.json.
    ///   2. Approximate the offset as `round(lng / 15)` hours (15° ≈ 1hr).
    ///   3. Fall back to `TimeZone.current` if the airport isn't in the DB.
    ///
    /// The 15°/hour approximation ignores political timezone boundaries and
    /// DST, but it's correct to within an hour of the real offset — plenty
    /// precise for *which calendar day* the `Date` falls on.
    private func dayString(from date: Date, originIATA: String) -> String {
        let f = DateFormatter()
        f.dateFormat = "yyyy-MM-dd"
        f.locale = Locale(identifier: "en_US_POSIX")
        f.timeZone = resolveTimeZone(forIATA: originIATA)
        return f.string(from: date)
    }

    /// Same as above but for Korean Air's compact "yyyyMMdd" format.
    private func compactDayString(from date: Date, originIATA: String) -> String {
        let f = DateFormatter()
        f.dateFormat = "yyyyMMdd"
        f.locale = Locale(identifier: "en_US_POSIX")
        f.timeZone = resolveTimeZone(forIATA: originIATA)
        return f.string(from: date)
    }

    private func resolveTimeZone(forIATA iata: String) -> TimeZone {
        guard let db = airportDatabase,
              let airport = db.airport(byIATA: iata.uppercased()) else {
            print("[TZ] \(iata): airport not in DB, falling back to TimeZone.current (\(TimeZone.current.identifier))")
            return .current
        }
        // Round lng/15 to nearest hour. Clamp to [-12, 14] (real-world range).
        let rawHours = (airport.lng / 15.0).rounded()
        let hours = max(-12, min(14, Int(rawHours)))
        let seconds = hours * 3600
        let tz = TimeZone(secondsFromGMT: seconds) ?? .current
        print("[TZ] \(iata) (lng=\(airport.lng)) → offset=\(hours)h (\(tz.identifier))")
        return tz
    }

    /// "yyyyMMdd" formatter for Korean Air
    private static let compactDateFormatter: DateFormatter = {
        let f = DateFormatter()
        f.dateFormat = "yyyyMMdd"
        f.locale = Locale(identifier: "en_US_POSIX")
        f.timeZone = TimeZone(identifier: "UTC")
        return f
    }()
}
