import Foundation

/// Live aircraft feed sourced from flightradar24.com's public
/// `/zones/fcgi/feed.js` endpoint. We use it as the primary live data
/// source because, unlike OpenSky's anonymous tier, FR24 aggregates
/// ASDE-X / MLAT / multiple ADS-B receivers and has solid ground
/// coverage at major airports — i.e. the parked SWA jet at DAL that
/// OpenSky just doesn't return.
///
/// Feed format (positional array per aircraft):
///   [0]  icao24 hex (uppercase)
///   [1]  latitude
///   [2]  longitude
///   [3]  heading (deg true)
///   [4]  altitude (feet, baro)
///   [5]  ground speed (knots)
///   [6]  squawk
///   [7]  radar source id (e.g. "T-KDFW42") — informational
///   [8]  ICAO aircraft type designator ("B738")
///   [9]  registration / tail number
///   [10] unix timestamp (seconds)
///   [11] departure airport IATA
///   [12] arrival airport IATA
///   [13] flight number with IATA carrier ("AA2152")
///   [14] on_ground (0/1)
///   [15] vertical rate (ft/min)
///   [16] callsign with ICAO carrier ("AAL2152")
///   [17] is_glider/special
///   [18] airline ICAO ("AAL")
///
/// The endpoint requires browser-shaped request headers (User-Agent +
/// Referer pointing at flightradar24.com). Plain curl is rejected.
actor FR24Client {
    enum ClientError: LocalizedError {
        case http(Int)
        case decode(String)
        case network(Error)

        var errorDescription: String? {
            switch self {
            case .http(let c): return "FR24 returned HTTP \(c)."
            case .decode(let s): return "Couldn't read FR24 response: \(s)."
            case .network(let e): return e.localizedDescription
            }
        }
    }

    private let session: URLSession

    init(session: URLSession = .shared) {
        self.session = session
    }

    /// Fetch every aircraft currently inside the bbox. The map view passes
    /// the visible region's corners — typical bbox at city zoom returns
    /// ~3–30 entries; continental zoom can return several hundred.
    func states(latMin: Double, lonMin: Double, latMax: Double, lonMax: Double) async throws -> [LiveAircraft] {
        // FR24 expects bounds in the order: latNorth, latSouth, lonWest, lonEast.
        let bounds = String(format: "%.4f,%.4f,%.4f,%.4f", latMax, latMin, lonMin, lonMax)

        var comps = URLComponents(string: "https://data-cloud.flightradar24.com/zones/fcgi/feed.js")!
        comps.queryItems = [
            URLQueryItem(name: "bounds",    value: bounds),
            URLQueryItem(name: "faa",       value: "1"),
            URLQueryItem(name: "satellite", value: "1"),
            URLQueryItem(name: "mlat",      value: "1"),
            URLQueryItem(name: "flarm",     value: "1"),
            URLQueryItem(name: "adsb",      value: "1"),
            URLQueryItem(name: "gnd",       value: "1"),
            URLQueryItem(name: "air",       value: "1"),
            URLQueryItem(name: "vehicles",  value: "1"),
            URLQueryItem(name: "estimated", value: "1"),
            URLQueryItem(name: "maxage",    value: "14400"),
            URLQueryItem(name: "gliders",   value: "1"),
            URLQueryItem(name: "stats",     value: "1"),
        ]

        var req = URLRequest(url: comps.url!)
        req.timeoutInterval = 12
        req.setValue(
            "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
            forHTTPHeaderField: "User-Agent"
        )
        req.setValue("https://www.flightradar24.com/", forHTTPHeaderField: "Referer")
        req.setValue("application/json",               forHTTPHeaderField: "Accept")

        let data: Data
        let response: URLResponse
        do {
            (data, response) = try await session.data(for: req)
        } catch {
            throw ClientError.network(error)
        }
        guard let http = response as? HTTPURLResponse else {
            throw ClientError.network(URLError(.badServerResponse))
        }
        guard (200..<300).contains(http.statusCode) else {
            throw ClientError.http(http.statusCode)
        }

        return try parse(data: data)
    }

    private func parse(data: Data) throws -> [LiveAircraft] {
        let root: Any
        do {
            root = try JSONSerialization.jsonObject(with: data, options: [])
        } catch {
            throw ClientError.decode("not json")
        }
        guard let dict = root as? [String: Any] else {
            throw ClientError.decode("root not object")
        }

        var out: [LiveAircraft] = []
        out.reserveCapacity(dict.count)
        for (_, value) in dict {
            guard let arr = value as? [Any] else { continue }
            if arr.count < 18 { continue }
            if let ac = Self.aircraft(from: arr) {
                out.append(ac)
            }
        }
        return out
    }

    /// Convert one positional entry into a LiveAircraft, returning nil
    /// when required fields are missing (no position, no icao24).
    private static func aircraft(from a: [Any]) -> LiveAircraft? {
        guard let icaoRaw = a[0] as? String, !icaoRaw.isEmpty,
              let lat = doubleVal(a[1]),
              let lon = doubleVal(a[2]) else {
            return nil
        }

        let heading      = doubleVal(a[3])
        let altFeet      = intVal(a[4]) ?? 0
        let speedKnots   = doubleVal(a[5])
        let squawkRaw    = a[6] as? String
        let modelType    = nonEmpty(a[8] as? String)
        let registration = nonEmpty(a[9] as? String)
        let timestamp    = intVal(a[10]) ?? Int(Date().timeIntervalSince1970)
        let depIATA      = nonEmpty(a[11] as? String)
        let arrIATA      = nonEmpty(a[12] as? String)
        let flightIATA   = nonEmpty(a[13] as? String)
        let onGround     = (intVal(a[14]) ?? 0) != 0
        let vertRateFpm  = doubleVal(a[15]) ?? 0
        let callsign     = nonEmpty(a[16] as? String)
        let airlineICAO  = nonEmpty(a.count > 18 ? a[18] as? String : nil)

        // Unit conversions — LiveAircraft stores baroAlt in meters,
        // velocity in m/s, vertical rate in m/s (matching OpenSky).
        let baroAltMeters: Double? = altFeet > 0 ? Double(altFeet) * 0.3048 : nil
        let velocityMps: Double? = speedKnots.map { $0 * 0.514444 }
        let vertRateMps: Double? = vertRateFpm != 0 ? vertRateFpm * 0.00508 : nil

        return LiveAircraft(
            icao24: icaoRaw.lowercased(),
            callsign: callsign,
            originCountry: "",
            latitude: lat,
            longitude: lon,
            baroAltitude: baroAltMeters,
            geoAltitude: nil,
            velocity: velocityMps,
            trueTrack: heading,
            verticalRate: vertRateMps,
            onGround: onGround,
            squawk: nonEmpty(squawkRaw),
            category: nil,
            lastContact: Date(timeIntervalSince1970: TimeInterval(timestamp)),
            enrichment: LiveAircraft.Enrichment(
                modelType: modelType,
                registration: registration,
                flightIATA: flightIATA,
                departureIATA: depIATA,
                arrivalIATA: arrIATA,
                airlineICAO: airlineICAO
            )
        )
    }

    private static func doubleVal(_ x: Any) -> Double? {
        if let d = x as? Double { return d }
        if let i = x as? Int    { return Double(i) }
        if let s = x as? String { return Double(s) }
        return nil
    }

    private static func intVal(_ x: Any) -> Int? {
        if let i = x as? Int    { return i }
        if let d = x as? Double { return Int(d) }
        if let s = x as? String { return Int(s) }
        return nil
    }

    private static func nonEmpty(_ s: String?) -> String? {
        guard let s, !s.isEmpty else { return nil }
        return s
    }
}
