import Foundation

/// Thin client for the OpenSky Network REST API. Two endpoints:
/// - `/states/all` — live aircraft state vectors (positions, velocity, etc.)
/// - `/flights/aircraft` — recent flight history per aircraft (used for the
///   tap-to-detail "where did this come from / going to" panel)
///
/// Anonymous access is rate-limited (~10s minimum between requests, 100/day);
/// authenticated access gets higher quotas but requires the user to register.
/// We default to anonymous + heavy debouncing in the UI layer.
actor OpenSkyClient {
    enum ClientError: Error, LocalizedError {
        case requestFailed(status: Int)
        case decodingFailed(Error)
        case throttled

        var errorDescription: String? {
            switch self {
            case .requestFailed(let s): return "OpenSky HTTP \(s)"
            case .decodingFailed(let e): return "Could not parse OpenSky response: \(e.localizedDescription)"
            case .throttled: return "OpenSky rate limit reached. Wait a moment and retry."
            }
        }
    }

    private let session: URLSession
    private var basicAuthHeader: String?

    init() {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 20
        config.requestCachePolicy = .reloadIgnoringLocalCacheData
        session = URLSession(configuration: config)
        basicAuthHeader = Self.makeBasicAuth(OpenSkyCredentials.shared.load())

        // Re-read credentials whenever the Settings screen saves new ones.
        NotificationCenter.default.addObserver(
            forName: .openSkyCredentialsChanged, object: nil, queue: nil
        ) { [weak self] _ in
            Task { await self?.reloadCredentials() }
        }
    }

    private func reloadCredentials() {
        basicAuthHeader = Self.makeBasicAuth(OpenSkyCredentials.shared.load())
    }

    nonisolated private static func makeBasicAuth(_ creds: OpenSkyCredentials.Credentials?) -> String? {
        guard let creds else { return nil }
        let raw = "\(creds.username):\(creds.password)"
        guard let data = raw.data(using: .utf8) else { return nil }
        return "Basic \(data.base64EncodedString())"
    }

    /// Apply auth header to a URLRequest if credentials are stored.
    private func applyAuth(_ request: inout URLRequest) {
        if let auth = basicAuthHeader {
            request.setValue(auth, forHTTPHeaderField: "Authorization")
        }
    }

    /// Aircraft inside the given lat/lon bounding box.
    ///
    /// Use the smallest bounding box that covers the user's visible map — too
    /// wide and you'll pull thousands of state vectors per call (and burn
    /// quota faster).
    func states(latMin: Double, lonMin: Double, latMax: Double, lonMax: Double) async throws -> [LiveAircraft] {
        var comps = URLComponents(string: "https://opensky-network.org/api/states/all")!
        comps.queryItems = [
            URLQueryItem(name: "lamin", value: String(latMin)),
            URLQueryItem(name: "lomin", value: String(lonMin)),
            URLQueryItem(name: "lamax", value: String(latMax)),
            URLQueryItem(name: "lomax", value: String(lonMax))
        ]
        guard let url = comps.url else { throw ClientError.requestFailed(status: -1) }
        return try await decodeStates(from: url)
    }

    private func decodeStates(from url: URL) async throws -> [LiveAircraft] {
        var req = URLRequest(url: url)
        applyAuth(&req)
        req.setValue("application/json", forHTTPHeaderField: "Accept")

        let (data, response) = try await session.data(for: req)
        let status = (response as? HTTPURLResponse)?.statusCode ?? -1
        if status == 429 { throw ClientError.throttled }
        guard status == 200 else { throw ClientError.requestFailed(status: status) }

        do {
            let resp = try JSONDecoder().decode(StatesResponse.self, from: data)
            return resp.states ?? []
        } catch {
            throw ClientError.decodingFailed(error)
        }
    }

    /// In-flight track for the given aircraft — sequence of (time, lat, lon,
    /// alt, heading, onGround) points covering the most recent flight (or
    /// current flight if it's still airborne). Used to draw the trail on
    /// the map when an aircraft is selected.
    func track(icao24: String) async -> AircraftTrack? {
        var comps = URLComponents(string: "https://opensky-network.org/api/tracks/all")!
        comps.queryItems = [
            URLQueryItem(name: "icao24", value: icao24.lowercased()),
            URLQueryItem(name: "time", value: "0")  // 0 = current/most-recent
        ]
        guard let url = comps.url else { return nil }

        var req = URLRequest(url: url)
        applyAuth(&req)
        req.setValue("application/json", forHTTPHeaderField: "Accept")

        do {
            let (data, response) = try await session.data(for: req)
            let status = (response as? HTTPURLResponse)?.statusCode ?? -1
            guard status == 200 else { return nil }
            return try? JSONDecoder().decode(AircraftTrack.self, from: data)
        } catch {
            return nil
        }
    }

    /// Flights an aircraft has flown in the past N days.
    /// OpenSky requires a `begin` and `end` window, max 30 days each.
    func recentFlights(icao24: String, daysBack: Int = 7) async -> [OpenSkyFlight] {
        let now = Int(Date().timeIntervalSince1970)
        let begin = now - (daysBack * 86400)
        var comps = URLComponents(string: "https://opensky-network.org/api/flights/aircraft")!
        comps.queryItems = [
            URLQueryItem(name: "icao24", value: icao24.lowercased()),
            URLQueryItem(name: "begin", value: String(begin)),
            URLQueryItem(name: "end", value: String(now))
        ]
        guard let url = comps.url else { return [] }

        var req = URLRequest(url: url)
        applyAuth(&req)
        req.setValue("application/json", forHTTPHeaderField: "Accept")

        do {
            let (data, response) = try await session.data(for: req)
            let status = (response as? HTTPURLResponse)?.statusCode ?? -1
            // 404 from OpenSky means "no flights in this window" — not an error.
            guard status == 200 else { return [] }
            return (try? JSONDecoder().decode([OpenSkyFlight].self, from: data)) ?? []
        } catch {
            return []
        }
    }
}

// MARK: - Decoding

/// OpenSky returns each state vector as a heterogeneous array — index 0 is
/// the ICAO24 hex, 1 is callsign, 2 is country, 3 is unix time of last
/// position, 4 is unix time of last contact, 5 is longitude, 6 is latitude,
/// 7 is barometric altitude in meters, 8 is on_ground bool, 9 is velocity
/// m/s, 10 is true_track degrees, 11 is vertical_rate m/s, 12 is sensors
/// (array of receiver IDs, skipped), 13 is geometric altitude meters, 14
/// is squawk, 15 is spi bool (skipped), 16 is position_source (skipped),
/// 17 is category. Most entries can be `null`.
private struct StatesResponse: Decodable {
    let time: Int
    let states: [LiveAircraft]?

    enum CodingKeys: String, CodingKey { case time, states }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        time = try c.decode(Int.self, forKey: .time)
        let raw = try c.decodeIfPresent([RawStateVector].self, forKey: .states)
        states = raw?.compactMap(LiveAircraft.from)
    }
}

/// Intermediate decoder that consumes the heterogeneous array into named
/// optional fields without exploding on null values or shape drift.
private struct RawStateVector: Decodable {
    let icao24: String
    let callsign: String?
    let originCountry: String
    let timePosition: Int?
    let lastContact: Int
    let longitude: Double?
    let latitude: Double?
    let baroAltitude: Double?
    let onGround: Bool
    let velocity: Double?
    let trueTrack: Double?
    let verticalRate: Double?
    let geoAltitude: Double?
    let squawk: String?
    let category: Int?

    init(from decoder: Decoder) throws {
        var c = try decoder.unkeyedContainer()
        icao24         = (try? c.decode(String.self)) ?? ""
        callsign       = try? c.decodeIfPresent(String.self)
        originCountry  = (try? c.decode(String.self)) ?? ""
        timePosition   = try? c.decodeIfPresent(Int.self)
        lastContact    = (try? c.decode(Int.self)) ?? 0
        longitude      = try? c.decodeIfPresent(Double.self)
        latitude       = try? c.decodeIfPresent(Double.self)
        baroAltitude   = try? c.decodeIfPresent(Double.self)
        onGround       = (try? c.decode(Bool.self)) ?? false
        velocity       = try? c.decodeIfPresent(Double.self)
        trueTrack      = try? c.decodeIfPresent(Double.self)
        verticalRate   = try? c.decodeIfPresent(Double.self)
        // [12] sensors array — skip.
        _              = try? c.decodeIfPresent([Int].self)
        geoAltitude    = try? c.decodeIfPresent(Double.self)
        squawk         = try? c.decodeIfPresent(String.self)
        // [15] spi, [16] position_source — skip.
        _              = try? c.decode(Bool.self)
        _              = try? c.decode(Int.self)
        category       = try? c.decodeIfPresent(Int.self)
    }
}

private extension LiveAircraft {
    static func from(_ raw: RawStateVector) -> LiveAircraft? {
        guard let lat = raw.latitude, let lon = raw.longitude, !raw.icao24.isEmpty else {
            return nil
        }
        return LiveAircraft(
            icao24: raw.icao24,
            callsign: raw.callsign,
            originCountry: raw.originCountry,
            latitude: lat,
            longitude: lon,
            baroAltitude: raw.baroAltitude,
            geoAltitude: raw.geoAltitude,
            velocity: raw.velocity,
            trueTrack: raw.trueTrack,
            verticalRate: raw.verticalRate,
            onGround: raw.onGround,
            squawk: raw.squawk,
            category: raw.category,
            lastContact: Date(timeIntervalSince1970: TimeInterval(raw.lastContact)),
            enrichment: nil
        )
    }
}
