import Foundation
import MapKit

final class AirportDatabase: Sendable {
    let airports: [MapAirport]
    private let regionNames: [String: String]  // "US-TX" -> "Texas"

    init() {
        guard let url = Bundle.main.url(forResource: "airports", withExtension: "json"),
              let data = try? Data(contentsOf: url),
              let decoded = try? JSONDecoder().decode([MapAirport].self, from: data)
        else {
            airports = []
            regionNames = [:]
            return
        }
        airports = decoded
        regionNames = Self.buildRegionNames()
    }

    /// Returns airports within the given map region, capped at `limit` for performance.
    func airports(in region: MKCoordinateRegion, limit: Int = 200) -> [MapAirport] {
        let centerLat = region.center.latitude
        let centerLng = region.center.longitude
        let latDelta = region.span.latitudeDelta / 2
        let lngDelta = region.span.longitudeDelta / 2

        let minLat = centerLat - latDelta
        let maxLat = centerLat + latDelta
        let minLng = centerLng - lngDelta
        let maxLng = centerLng + lngDelta

        var result: [MapAirport] = []
        for airport in airports {
            guard airport.lat >= minLat, airport.lat <= maxLat else { continue }

            if minLng <= maxLng {
                guard airport.lng >= minLng, airport.lng <= maxLng else { continue }
            } else {
                guard airport.lng >= minLng || airport.lng <= maxLng else { continue }
            }

            result.append(airport)
            if result.count >= limit { break }
        }

        return result
    }

    /// Search airports by region/state name (e.g. "Texas" -> all US-TX airports).
    /// Returns matching region name and airports, or nil if no match.
    func searchByRegion(term: String) -> (regionName: String, airports: [MapAirport])? {
        let lowered = term.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
        guard lowered.count >= 3 else { return nil }

        // Find matching region
        var matchedCode: String?
        var matchedName: String?

        for (code, name) in regionNames {
            if name.lowercased().hasPrefix(lowered) || name.lowercased() == lowered {
                matchedCode = code
                matchedName = name
                break
            }
        }

        guard let code = matchedCode, let name = matchedName else { return nil }

        let results = airports.filter { $0.region == code }
            .sorted { $0.name.localizedCompare($1.name) == .orderedAscending }

        guard !results.isEmpty else { return nil }
        return (regionName: name, airports: results)
    }

    /// Look up a single airport by IATA code
    func airport(byIATA code: String) -> MapAirport? {
        airports.first { $0.iata == code }
    }

    /// Resolve a 4-letter ICAO code (e.g. "KDFW", "EGLL") to its IATA
    /// equivalent. Returns nil when the ICAO doesn't map to an airport we
    /// know about — callers should NOT pretend an unknown ICAO is a valid
    /// IATA (silent fallthrough downstream looks up against an empty
    /// table and surfaces nothing in the UI).
    ///
    /// Strategy:
    ///   1. Prefix-drop heuristic for the regions where it's deterministic:
    ///      US "Kxxx" → "xxx", Canada "CYxx" → "Yxx", Mexico "MMxx" → "Mxx".
    ///      Verify the result against the bundled airport list so an
    ///      accidental KFOO doesn't silently masquerade as "FOO".
    ///   2. Otherwise consult the curated ``icaoToIATA`` table below
    ///      (major intl hubs that the BTS bundle / live tab can surface).
    func iata(forICAO icao: String) -> String? {
        let raw = icao.uppercased()
        guard raw.count == 4 else { return nil }

        // Regional prefix-drop (US / CA / MX) → must round-trip through
        // the airport list to count as a valid mapping.
        var candidate: String?
        if raw.hasPrefix("K") {
            candidate = String(raw.dropFirst())
        } else if raw.hasPrefix("CY") {
            candidate = String(raw.dropFirst())
        } else if raw.hasPrefix("MM") {
            candidate = String(raw.dropFirst())
        }
        if let c = candidate, airport(byIATA: c) != nil { return c }

        if let mapped = Self.icaoToIATA[raw] { return mapped }
        return nil
    }

    /// Look up an airport directly by its 4-letter ICAO code.
    /// Returns nil when the mapping can't be resolved.
    func airport(byICAO code: String) -> MapAirport? {
        guard let iata = iata(forICAO: code) else { return nil }
        return airport(byIATA: iata)
    }

    /// Resolve a 3-letter IATA code to its 4-letter ICAO code. Reverse of
    /// ``iata(forICAO:)``. Used by FlightAware-based lookups, whose URLs
    /// take ICAO airport codes (`KDFW`, `EHAM`).
    ///
    /// Strategy:
    ///   1. Check the inverted curated table — covers international hubs
    ///      and Alaska/Hawaii/territory ICAOs that don't follow the
    ///      simple prefix rule (e.g. ANC→PANC, HNL→PHNL, MEX→MMMX).
    ///   2. Deterministic prefix for US 48 states and Canada, gated by
    ///      the bundled airport list's `region` so we don't synthesize a
    ///      bogus ICAO for an IATA that isn't actually a US/CA airport.
    func icao(forIATA iata: String) -> String? {
        let upper = iata.uppercased()
        guard upper.count == 3 else { return nil }
        if let mapped = Self.iataToICAO[upper] { return mapped }
        guard let airport = airport(byIATA: upper) else { return nil }
        let region = airport.region
        if region.hasPrefix("US-") { return "K" + upper }
        if region.hasPrefix("CA-") { return "C" + upper }
        return nil
    }

    /// Inverted ``icaoToIATA`` so ``icao(forIATA:)`` is O(1). Computed once
    /// at first access.
    private static let iataToICAO: [String: String] = {
        var inverse: [String: String] = [:]
        for (icao, iata) in icaoToIATA {
            inverse[iata] = icao
        }
        return inverse
    }()

    /// Curated ICAO → IATA mappings for major hubs outside the
    /// deterministic-prefix regions. Sourced from publicly published
    /// airport directories (OurAirports, IATA airport directory) and
    /// limited to airports a flight surfaced by FR24/OpenSky on the Live
    /// tab is likely to reference.
    private static let icaoToIATA: [String: String] = [
        // United Kingdom & Ireland
        "EGLL": "LHR", "EGKK": "LGW", "EGSS": "STN", "EGGW": "LTN",
        "EGCC": "MAN", "EGPH": "EDI", "EGPF": "GLA", "EGBB": "BHX",
        "EGNT": "NCL", "EGNX": "EMA", "EGAA": "BFS", "EGAC": "BHD",
        "EIDW": "DUB", "EICK": "ORK", "EINN": "SNN",
        // France
        "LFPG": "CDG", "LFPO": "ORY", "LFBO": "TLS", "LFLL": "LYS",
        "LFMN": "NCE", "LFML": "MRS", "LFRS": "NTE", "LFBD": "BOD",
        "LFSB": "BSL",
        // Germany
        "EDDF": "FRA", "EDDM": "MUC", "EDDB": "BER", "EDDH": "HAM",
        "EDDL": "DUS", "EDDK": "CGN", "EDDS": "STR", "EDDN": "NUE",
        // Netherlands / Belgium / Luxembourg
        "EHAM": "AMS", "EHRD": "RTM", "EHEH": "EIN",
        "EBBR": "BRU", "EBCI": "CRL", "EBLG": "LGG", "EBAW": "ANR",
        "ELLX": "LUX",
        // Switzerland / Austria
        "LSZH": "ZRH", "LSGG": "GVA", "LSZB": "BRN",
        "LOWW": "VIE", "LOWS": "SZG", "LOWI": "INN",
        // Spain / Portugal
        "LEMD": "MAD", "LEBL": "BCN", "LEMG": "AGP", "LEPA": "PMI",
        "LEVC": "VLC", "LEAL": "ALC", "LEBB": "BIO", "LEZL": "SVQ",
        "LEST": "SCQ", "GCLP": "LPA", "GCTS": "TFS", "GCXO": "TFN",
        "LPPT": "LIS", "LPPR": "OPO", "LPFR": "FAO", "LPMA": "FNC",
        // Italy / Greece / Malta / Turkey
        "LIRF": "FCO", "LIMC": "MXP", "LIML": "LIN", "LIPZ": "VCE",
        "LIRA": "CIA", "LIRN": "NAP", "LIPE": "BLQ", "LIME": "BGY",
        "LICC": "CTA", "LICJ": "PMO", "LIEO": "OLB",
        "LGAV": "ATH", "LGTS": "SKG", "LGIR": "HER", "LGRP": "RHO",
        "LMML": "MLA",
        "LTBA": "ISL", "LTFM": "IST", "LTAC": "ESB", "LTAI": "AYT",
        // Nordics
        "ESSA": "ARN", "ESGG": "GOT", "ESMS": "MMX",
        "EKCH": "CPH", "EKBI": "BLL", "EKAH": "AAR",
        "ENGM": "OSL", "ENBR": "BGO", "ENZV": "SVG", "ENTC": "TOS",
        "EFHK": "HEL", "EFRO": "RVN", "EFKU": "KUO",
        "BIKF": "KEF",
        // Eastern Europe / Russia
        "EPWA": "WAW", "EPKK": "KRK", "EPGD": "GDN", "EPPO": "POZ",
        "LKPR": "PRG", "LZIB": "BTS", "LHBP": "BUD",
        "LROP": "OTP", "LBSF": "SOF", "LWSK": "SKP",
        "EYVI": "VNO", "EVRA": "RIX", "EETN": "TLL",
        "UUEE": "SVO", "UUDD": "DME", "UUWW": "VKO",
        "ULLI": "LED",
        // Middle East
        "OMDB": "DXB", "OMAA": "AUH", "OMSJ": "SHJ",
        "OTHH": "DOH", "OOMS": "MCT", "OBBI": "BAH",
        "OKBK": "KWI", "OERK": "RUH", "OEJN": "JED",
        "LLBG": "TLV", "OJAI": "AMM",
        // Africa
        "HECA": "CAI", "GMMN": "CMN", "DAAG": "ALG", "DTTA": "TUN",
        "HAAB": "ADD", "HKJK": "NBO", "DNMM": "LOS", "DGAA": "ACC",
        "FAOR": "JNB", "FACT": "CPT", "FADN": "DUR",
        // South Africa / Indian Ocean
        "FIMP": "MRU", "FMEE": "RUN",
        // South Asia
        "VABB": "BOM", "VIDP": "DEL", "VECC": "CCU", "VOMM": "MAA",
        "VOBL": "BLR", "VOHS": "HYD", "VOCI": "COK", "VOTV": "TRV",
        "VAAH": "AMD", "VOTR": "TIR",
        "VCBI": "CMB",
        "VGHS": "DAC",
        "OPKC": "KHI", "OPLA": "LHE", "OPIS": "ISB",
        // SE Asia / Pacific
        "WSSS": "SIN",
        "WMKK": "KUL", "WMSA": "SZB",
        "VTBS": "BKK", "VTBD": "DMK", "VTSP": "HKT", "VTCC": "CNX",
        "VVNB": "HAN", "VVTS": "SGN", "VVDN": "DAD",
        "WIII": "CGK", "WADD": "DPS", "WICC": "BDO", "WARR": "SUB",
        "WAJJ": "DJJ",
        "RPLL": "MNL", "RPVM": "CEB", "RPVI": "ILO",
        "VLVT": "VTE",
        "VYYY": "RGN",
        "VDPP": "PNH", "VDSR": "REP",
        // North Asia
        "ZBAA": "PEK", "ZBAD": "PKX", "ZSPD": "PVG", "ZSSS": "SHA",
        "ZGGG": "CAN", "ZGSZ": "SZX", "ZUUU": "CTU", "ZGOW": "SWA",
        "ZBTJ": "TSN", "ZSHC": "HGH", "ZSAM": "XMN", "ZGHA": "CSX",
        "ZGKL": "KWL", "ZHHH": "WUH", "ZWWW": "URC",
        "VHHH": "HKG", "VMMC": "MFM",
        "RCTP": "TPE", "RCSS": "TSA", "RCKH": "KHH",
        "RKSI": "ICN", "RKSS": "GMP", "RKPK": "PUS", "RKPC": "CJU",
        "RJTT": "HND", "RJAA": "NRT", "RJBB": "KIX", "RJOO": "ITM",
        "RJCC": "CTS", "RJFF": "FUK", "RJOA": "HIJ", "RJGG": "NGO",
        "RJOM": "MYJ", "RJSS": "SDJ",
        "RJNA": "NGO",
        // Australia / Oceania
        "YSSY": "SYD", "YMML": "MEL", "YBBN": "BNE", "YPPH": "PER",
        "YPAD": "ADL", "YBCG": "OOL", "YBCS": "CNS", "YPDN": "DRW",
        "YPJT": "JT0",
        "NZAA": "AKL", "NZCH": "CHC", "NZWN": "WLG", "NZQN": "ZQN",
        "NFFN": "NAN", "NFTF": "TBU", "NTAA": "PPT",
        "FAOL": "OOL",
        // Latin America
        "MMMX": "MEX", "MMUN": "CUN", "MMGL": "GDL", "MMMY": "MTY",
        "MMTJ": "TIJ",
        "MROC": "SJO", "MGGT": "GUA", "MSLP": "SAL", "MNMG": "MGA",
        "MPTO": "PTY", "MUHA": "HAV", "MDPC": "PUJ", "MDSD": "SDQ",
        "TJSJ": "SJU",
        "SBGR": "GRU", "SBSP": "GRU", "SBKP": "VCP", "SBGL": "GIG",
        "SBSV": "SSA", "SBRF": "REC", "SBFZ": "FOR", "SBBR": "BSB",
        "SBPA": "POA", "SBCT": "CWB", "SBFL": "FLN", "SBBE": "BEL",
        "SBMN": "MAO",
        "SAEZ": "EZE", "SABE": "AEP", "SCEL": "SCL", "SPJC": "LIM",
        "SUMU": "MVD", "SKBO": "BOG", "SKCL": "CLO", "SKRG": "MDE",
        "SEQM": "UIO", "SVMI": "CCS",
    ]

    /// Return the IANA timezone for an airport's IATA code, or nil if we
    /// don't have a confident mapping. Used by ``LoadFactorService`` so
    /// weekday + month adjustments resolve in airport-local time rather
    /// than UTC (otherwise late-evening west-coast departures roll past
    /// midnight UTC and lose the weekend bump).
    ///
    /// The table is curated to major US carrier airports plus a handful
    /// of common Canadian and international hubs — enough to cover every
    /// airport the bundled BTS data references. Anything we don't know
    /// returns nil so callers can fall back to UTC explicitly.
    func timeZone(forIATA code: String) -> TimeZone? {
        guard let id = Self.iataTimeZoneMap[code.uppercased()] else {
            return nil
        }
        return TimeZone(identifier: id)
    }

    /// Curated IATA → IANA timezone identifier table. Sourced from
    /// publicly published airport timezone references (OurAirports,
    /// IATA airport directory). Only includes airports referenced by
    /// the bundled BTS data or common nonrev itineraries.
    private static let iataTimeZoneMap: [String: String] = [
        // Pacific
        "SEA": "America/Los_Angeles", "PDX": "America/Los_Angeles",
        "SFO": "America/Los_Angeles", "OAK": "America/Los_Angeles",
        "SJC": "America/Los_Angeles", "LAX": "America/Los_Angeles",
        "BUR": "America/Los_Angeles", "ONT": "America/Los_Angeles",
        "SAN": "America/Los_Angeles", "SNA": "America/Los_Angeles",
        "LGB": "America/Los_Angeles", "PSP": "America/Los_Angeles",
        "FAT": "America/Los_Angeles", "SMF": "America/Los_Angeles",
        "RNO": "America/Los_Angeles", "LAS": "America/Los_Angeles",
        // Mountain
        "PHX": "America/Phoenix", "TUS": "America/Phoenix",
        "DEN": "America/Denver", "COS": "America/Denver",
        "ABQ": "America/Denver", "SLC": "America/Denver",
        "BOI": "America/Boise", "BIL": "America/Denver",
        "MSO": "America/Denver", "ELP": "America/Denver",
        // Central
        "DFW": "America/Chicago", "DAL": "America/Chicago",
        "IAH": "America/Chicago", "HOU": "America/Chicago",
        "AUS": "America/Chicago", "SAT": "America/Chicago",
        "MSY": "America/Chicago", "MEM": "America/Chicago",
        "BNA": "America/Chicago", "STL": "America/Chicago",
        "MCI": "America/Chicago", "MSP": "America/Chicago",
        "ORD": "America/Chicago", "MDW": "America/Chicago",
        "MKE": "America/Chicago", "OMA": "America/Chicago",
        "OKC": "America/Chicago", "TUL": "America/Chicago",
        "LIT": "America/Chicago", "JAN": "America/Chicago",
        "BHM": "America/Chicago", "HSV": "America/Chicago",
        "MOB": "America/Chicago", "SHV": "America/Chicago",
        "LRD": "America/Chicago", "BRO": "America/Chicago",
        "MFE": "America/Chicago", "CRP": "America/Chicago",
        "LBB": "America/Chicago", "AMA": "America/Chicago",
        "MAF": "America/Chicago", "ICT": "America/Chicago",
        // Eastern
        "ATL": "America/New_York", "CLT": "America/New_York",
        "RDU": "America/New_York", "DCA": "America/New_York",
        "IAD": "America/New_York", "BWI": "America/New_York",
        "PHL": "America/New_York", "EWR": "America/New_York",
        "JFK": "America/New_York", "LGA": "America/New_York",
        "BOS": "America/New_York", "PVD": "America/New_York",
        "MHT": "America/New_York", "PWM": "America/New_York",
        "BGR": "America/New_York", "BTV": "America/New_York",
        "BUF": "America/New_York", "ROC": "America/New_York",
        "SYR": "America/New_York", "ALB": "America/New_York",
        "PIT": "America/New_York", "CLE": "America/New_York",
        "CMH": "America/New_York", "CVG": "America/New_York",
        "DTW": "America/New_York", "IND": "America/New_York",
        "SDF": "America/New_York", "LEX": "America/New_York",
        "RIC": "America/New_York", "ORF": "America/New_York",
        "ROA": "America/New_York", "GSO": "America/New_York",
        "CHS": "America/New_York", "CAE": "America/New_York",
        "GSP": "America/New_York", "AVL": "America/New_York",
        "MYR": "America/New_York", "ILM": "America/New_York",
        "SAV": "America/New_York", "JAX": "America/New_York",
        "TLH": "America/New_York", "MCO": "America/New_York",
        "TPA": "America/New_York", "PIE": "America/New_York",
        "RSW": "America/New_York", "MIA": "America/New_York",
        "FLL": "America/New_York", "PBI": "America/New_York",
        "EYW": "America/New_York", "PNS": "America/New_York",
        "VPS": "America/New_York", "ECP": "America/New_York",
        // Alaska / Hawaii
        "ANC": "America/Anchorage", "FAI": "America/Anchorage",
        "JNU": "America/Juneau", "KTN": "America/Sitka",
        "HNL": "Pacific/Honolulu", "OGG": "Pacific/Honolulu",
        "KOA": "Pacific/Honolulu", "LIH": "Pacific/Honolulu",
        "ITO": "Pacific/Honolulu",
        // Caribbean / Territories
        "SJU": "America/Puerto_Rico", "BQN": "America/Puerto_Rico",
        "PSE": "America/Puerto_Rico", "STT": "America/Puerto_Rico",
        "STX": "America/Puerto_Rico",
        // Canada (most common cross-border)
        "YYZ": "America/Toronto", "YOW": "America/Toronto",
        "YUL": "America/Toronto", "YHZ": "America/Halifax",
        "YYC": "America/Edmonton", "YEG": "America/Edmonton",
        "YVR": "America/Vancouver", "YWG": "America/Winnipeg",
    ]

    /// Return the airport closest to a given coordinate, optionally
    /// within a max distance. Linear scan — O(n) with ~3,900 airports,
    /// fast enough on the main thread for tap-then-lookup flows.
    func nearestAirport(to coordinate: CLLocationCoordinate2D, maxMiles: Double = 25) -> MapAirport? {
        guard !airports.isEmpty else { return nil }
        var bestAirport: MapAirport?
        var bestDistSq: Double = .greatestFiniteMagnitude
        // Convert max miles to (degrees lat)² in a rough planar sense — good
        // enough for "nearest airport" filtering. ~1 degree lat ≈ 69 miles.
        let cutoffSq = (maxMiles / 69.0) * (maxMiles / 69.0)
        for ap in airports {
            let dLat = ap.lat - coordinate.latitude
            let dLng = ap.lng - coordinate.longitude
            let dSq  = dLat * dLat + dLng * dLng
            if dSq < bestDistSq {
                bestDistSq = dSq
                bestAirport = ap
            }
        }
        return bestDistSq <= cutoffSq ? bestAirport : nil
    }

    private static func buildRegionNames() -> [String: String] {
        // US states + territories
        var names: [String: String] = [
            "US-AL": "Alabama", "US-AK": "Alaska", "US-AZ": "Arizona", "US-AR": "Arkansas",
            "US-CA": "California", "US-CO": "Colorado", "US-CT": "Connecticut", "US-DE": "Delaware",
            "US-FL": "Florida", "US-GA": "Georgia", "US-HI": "Hawaii", "US-ID": "Idaho",
            "US-IL": "Illinois", "US-IN": "Indiana", "US-IA": "Iowa", "US-KS": "Kansas",
            "US-KY": "Kentucky", "US-LA": "Louisiana", "US-ME": "Maine", "US-MD": "Maryland",
            "US-MA": "Massachusetts", "US-MI": "Michigan", "US-MN": "Minnesota", "US-MS": "Mississippi",
            "US-MO": "Missouri", "US-MT": "Montana", "US-NE": "Nebraska", "US-NV": "Nevada",
            "US-NH": "New Hampshire", "US-NJ": "New Jersey", "US-NM": "New Mexico", "US-NY": "New York",
            "US-NC": "North Carolina", "US-ND": "North Dakota", "US-OH": "Ohio", "US-OK": "Oklahoma",
            "US-OR": "Oregon", "US-PA": "Pennsylvania", "US-RI": "Rhode Island", "US-SC": "South Carolina",
            "US-SD": "South Dakota", "US-TN": "Tennessee", "US-TX": "Texas", "US-UT": "Utah",
            "US-VT": "Vermont", "US-VA": "Virginia", "US-WA": "Washington", "US-WV": "West Virginia",
            "US-WI": "Wisconsin", "US-WY": "Wyoming", "US-DC": "District of Columbia",
            "US-PR": "Puerto Rico", "US-VI": "U.S. Virgin Islands", "US-GU": "Guam",
            "US-AS": "American Samoa",
            // Canadian provinces
            "CA-AB": "Alberta", "CA-BC": "British Columbia", "CA-MB": "Manitoba",
            "CA-NB": "New Brunswick", "CA-NL": "Newfoundland and Labrador",
            "CA-NS": "Nova Scotia", "CA-NT": "Northwest Territories", "CA-NU": "Nunavut",
            "CA-ON": "Ontario", "CA-PE": "Prince Edward Island", "CA-QC": "Quebec",
            "CA-SK": "Saskatchewan", "CA-YT": "Yukon",
            // Australian states
            "AU-NSW": "New South Wales", "AU-VIC": "Victoria", "AU-QLD": "Queensland",
            "AU-WA": "Western Australia", "AU-SA": "South Australia", "AU-TAS": "Tasmania",
            "AU-NT": "Northern Territory", "AU-ACT": "Australian Capital Territory",
        ]
        return names
    }
}
