import Foundation
import CoreLocation

/// One aircraft's live state vector, normalized from OpenSky's `/states/all`
/// positional array format into a typed struct.
struct LiveAircraft: Identifiable, Hashable, Sendable {
    var id: String { icao24 }

    /// 24-bit ICAO transponder address as hex (lowercased).
    let icao24: String

    /// ADS-B broadcast callsign, e.g. `DAL1234` (ICAO airline code + flight number).
    /// Often padded with trailing whitespace — `trimmedCallsign` strips that.
    let callsign: String?

    /// ICAO-registered country of the operator.
    let originCountry: String

    let latitude: Double
    let longitude: Double

    /// Barometric altitude in meters. Falls back to geometric altitude in
    /// `altitudeFeet`.
    let baroAltitude: Double?
    let geoAltitude: Double?

    /// Velocity in m/s.
    let velocity: Double?

    /// True track in degrees from North (0..360).
    let trueTrack: Double?

    /// Vertical rate in m/s; positive = climbing.
    let verticalRate: Double?

    let onGround: Bool
    let squawk: String?

    /// Aircraft category from ADS-B emitter category (0–7). Decodes per
    /// `aircraftCategoryName`.
    let category: Int?

    /// When the position was last updated (server-side).
    let lastContact: Date

    /// Extra fields the FR24 feed provides inline (departure/arrival IATA,
    /// flight number, aircraft model, tail number, airline ICAO). Always
    /// nil for aircraft sourced from OpenSky.
    let enrichment: Enrichment?

    /// FR24-only inline data. None of these are guaranteed even when the
    /// outer envelope is FR24-sourced — gate aircraft often have no
    /// flight number, GA aircraft no airline, etc.
    struct Enrichment: Hashable, Sendable {
        let modelType: String?     // ICAO type designator, e.g. "B738"
        let registration: String?  // Tail number, e.g. "N971NN"
        let flightIATA: String?    // "AA2152"
        let departureIATA: String? // "DFW"
        let arrivalIATA: String?   // "MSP"
        let airlineICAO: String?   // "AAL"
    }

    var coordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    }

    var altitudeFeet: Int? {
        guard let alt = baroAltitude ?? geoAltitude else { return nil }
        return Int(alt * 3.28084)
    }

    var velocityKnots: Int? {
        guard let v = velocity else { return nil }
        return Int(v * 1.94384)
    }

    var heading: Int? {
        guard let t = trueTrack else { return nil }
        return Int(t.truncatingRemainder(dividingBy: 360))
    }

    var verticalState: VerticalState {
        guard let vr = verticalRate else { return .level }
        if vr > 1.5  { return .climbing }
        if vr < -1.5 { return .descending }
        return .level
    }

    /// ICAO aircraft type designator (e.g. "B738", "A21N"). Prefers the
    /// FR24-supplied model when present (more accurate, includes
    /// recent retrofits), else falls back to the bundled DB lookup.
    var typeCode: String? {
        if let m = enrichment?.modelType, !m.isEmpty { return m }
        return AircraftDatabase.shared.typeCode(forICAO24: icao24)
    }

    var trimmedCallsign: String? {
        let s = (callsign ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
        return s.isEmpty ? nil : s
    }

    /// 3-letter ICAO airline prefix. Prefers the FR24-supplied value when
    /// available — it correctly identifies SWA at "AA0013" style callsigns
    /// where the prefix derivation would fail. Falls back to extracting
    /// the leading letters from the callsign (works for OpenSky-style
    /// "AAL2152").
    var airlineICAO: String? {
        if let a = enrichment?.airlineICAO, !a.isEmpty { return a }
        guard let cs = trimmedCallsign else { return nil }
        let letters = cs.prefix(while: { $0.isLetter })
        guard letters.count == 3 else { return nil }
        return String(letters)
    }

    /// Numeric flight number portion (everything after the airline prefix).
    var flightNumber: String? {
        guard let cs = trimmedCallsign else { return nil }
        let s = String(cs.drop(while: { $0.isLetter }))
        return s.isEmpty ? nil : s
    }
}

enum VerticalState {
    case climbing, descending, level
}

/// ADS-B emitter category, 1–7, per RTCA DO-260.
func aircraftCategoryName(_ code: Int?) -> String? {
    switch code {
    case 1: return "Light"
    case 2: return "Small"
    case 3: return "Large"
    case 4: return "High vortex large"
    case 5: return "Heavy"
    case 6: return "High performance"
    case 7: return "Rotorcraft"
    default: return nil
    }
}

/// In-flight position track for one aircraft, returned by OpenSky's
/// `/tracks/all?icao24=...&time=0` endpoint. Each path entry is a
/// `[time, lat, lon, baroAlt, trueTrack, onGround]` heterogeneous array,
/// which we decode into a typed `TrackPoint`.
struct AircraftTrack: Decodable, Sendable {
    let icao24: String
    let callsign: String?
    let startTime: Int
    let endTime: Int
    let path: [TrackPoint]

    struct TrackPoint: Decodable, Sendable, Hashable {
        let time: Int
        let latitude: Double
        let longitude: Double
        let baroAltitude: Double?
        let trueTrack: Double?
        let onGround: Bool

        init(from decoder: Decoder) throws {
            var c = try decoder.unkeyedContainer()
            time         = (try? c.decode(Int.self)) ?? 0
            latitude     = (try? c.decodeIfPresent(Double.self)) ?? 0
            longitude    = (try? c.decodeIfPresent(Double.self)) ?? 0
            baroAltitude = try? c.decodeIfPresent(Double.self)
            trueTrack    = try? c.decodeIfPresent(Double.self)
            onGround     = (try? c.decode(Bool.self)) ?? false
        }
    }
}

/// Historical OpenSky flight record — used to surface "where did this aircraft
/// take off from / where's it going" in the detail sheet.
struct OpenSkyFlight: Decodable, Sendable, Hashable {
    let icao24: String
    let firstSeen: Int
    let lastSeen: Int
    let estDepartureAirport: String?
    let estArrivalAirport: String?
    let callsign: String?

    var departureDate: Date { Date(timeIntervalSince1970: TimeInterval(firstSeen)) }
    var arrivalDate: Date { Date(timeIntervalSince1970: TimeInterval(lastSeen)) }
    var trimmedCallsign: String? {
        let s = (callsign ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
        return s.isEmpty ? nil : s
    }
}
