import Foundation

/// Compares the scheduled aircraft type for a flight against what's actually flying
/// it today. Surfaces equipment swaps that matter for standby travelers — a smaller
/// bird means fewer open seats, a bigger one means better odds.
actor EquipmentSwapService {

    static let shared = EquipmentSwapService()

    // MARK: - Public types

    enum SwapSeverity: Sendable {
        case none
        case minor
        case significant
    }

    struct EquipmentSwapResult: Sendable {
        let scheduledName: String
        let scheduledSeats: Int
        let liveName: String?
        let liveSeats: Int?
        let seatDelta: Int?
        let severity: SwapSeverity
        let summary: String
    }

    // MARK: - JSON model
    //
    // Phase-2 schema (schemaVersion 2) nests seat counts per carrier:
    //   iata.<code>.default       — generic fallback (name + seats + body)
    //   iata.<code>.byCarrier.<C> — per-carrier override (seats + cabins + source)
    //
    // We keep back-compat with the original flat schema (where each
    // IATA mapped directly to {name, seats, body}) by trying that decode
    // path if the nested form isn't present.

    private struct CarrierSeats: Decodable {
        let seats: Int
    }

    private struct DefaultSeats: Decodable {
        let name: String
        let seats: Int
        let body: String?
    }

    /// One IATA entry. Either nested (default + byCarrier) or flat (name/seats/body
    /// at the top level). We decode both shapes.
    private struct IATAEntry: Decodable {
        let `default`: DefaultSeats?
        let byCarrier: [String: CarrierSeats]?

        // Flat back-compat fields.
        let flatName: String?
        let flatSeats: Int?
        let flatBody: String?

        private enum CodingKeys: String, CodingKey {
            case `default`
            case byCarrier
            case name
            case seats
            case body
        }

        init(from decoder: Decoder) throws {
            let c = try decoder.container(keyedBy: CodingKeys.self)
            self.default = try c.decodeIfPresent(DefaultSeats.self, forKey: .default)
            self.byCarrier = try c.decodeIfPresent([String: CarrierSeats].self, forKey: .byCarrier)
            self.flatName = try c.decodeIfPresent(String.self, forKey: .name)
            self.flatSeats = try c.decodeIfPresent(Int.self, forKey: .seats)
            self.flatBody = try c.decodeIfPresent(String.self, forKey: .body)
        }

        /// Display name pulled from whichever shape decoded.
        var displayName: String? {
            self.default?.name ?? flatName
        }

        /// Body code if present (kept for symmetry; not currently surfaced).
        var bodyType: String? {
            self.default?.body ?? flatBody
        }

        /// Default seat count — used when no carrier match.
        var defaultSeats: Int? {
            self.default?.seats ?? flatSeats
        }

        /// Look up carrier-specific seats with fallback to default.
        func seats(forCarrier carrier: String?) -> Int? {
            if let carrier = carrier?.uppercased(),
               let perCarrier = byCarrier?[carrier]?.seats {
                return perCarrier
            }
            return defaultSeats
        }
    }

    private struct Catalog: Decodable {
        let iata: [String: IATAEntry]
        let icao: [String: String]
    }

    // MARK: - State

    private var catalog: Catalog?
    private var didAttemptLoad = false

    // MARK: - Public API

    /// Compares scheduled vs live equipment. Returns `nil` only when both inputs are nil.
    /// If the scheduled type can't be resolved against the catalog the call also returns nil
    /// (we have nothing meaningful to say without a baseline).
    ///
    /// `carrier` is the operating airline's IATA code; when supplied we prefer
    /// `byCarrier[carrier].seats` over the generic default seat count.
    ///
    /// `btsBaselineSeats` is an optional fallback for the **scheduled** seat
    /// count: when the caller doesn't have an explicit scheduled-equipment
    /// IATA (e.g. FR24-sourced live flights where only the operating ICAO
    /// type is known) but does have a BTS historical seat-count for the
    /// route, the comparison is still meaningful — live aircraft vs.
    /// the route's typical aircraft size. The card surfaces with a
    /// "Typical equipment" label instead of a specific scheduled type.
    func check(
        scheduledEquipmentIATA: String?,
        liveEquipmentICAO: String?,
        carrier: String? = nil,
        btsBaselineSeats: Int? = nil
    ) async -> EquipmentSwapResult? {
        if scheduledEquipmentIATA == nil && liveEquipmentICAO == nil {
            return nil
        }

        loadIfNeeded()
        guard let catalog else {
            print("[EquipmentSwap] catalog unavailable — bailing")
            return nil
        }

        let scheduledKey = scheduledEquipmentIATA?.uppercased()
        let liveIATAKey = Self.iataKey(forICAO: liveEquipmentICAO, catalog: catalog)

        // Resolve the scheduled baseline. Three sources, in priority order:
        //   1. An explicit scheduled IATA that resolves against the catalog.
        //   2. A BTS-derived typical seat count for the route — used when
        //      we only have an FR24 flight number / route and no real
        //      scheduled equipment, so the comparison becomes "today's
        //      aircraft vs the route's historical typical".
        //   3. Nothing → bail.
        let scheduledName: String
        let scheduledSeats: Int

        if let scheduledKey,
           let scheduledEntry = catalog.iata[scheduledKey],
           let entryName = scheduledEntry.displayName,
           let entrySeats = scheduledEntry.seats(forCarrier: carrier) {
            scheduledName = entryName
            scheduledSeats = entrySeats
        } else if let baseline = btsBaselineSeats, baseline > 0 {
            scheduledName = "Typical equipment for this route"
            scheduledSeats = baseline
        } else {
            print("[EquipmentSwap] no scheduled baseline (catalog miss + no BTS) for \(scheduledEquipmentIATA ?? "nil")")
            return nil
        }

        let liveEntry: IATAEntry? = liveIATAKey.flatMap { catalog.iata[$0] }
        let liveName = liveEntry?.displayName
        let liveSeats = liveEntry?.seats(forCarrier: carrier)
        let seatDelta: Int? = liveSeats.map { $0 - scheduledSeats }
        let severity: SwapSeverity = {
            guard let seatDelta else { return .none }
            let magnitude = abs(seatDelta)
            if magnitude == 0 { return .none }
            if magnitude > 15 { return .significant }
            return .minor
        }()

        let summary = Self.summary(
            scheduledKey: scheduledKey ?? "typical",
            scheduledSeats: scheduledSeats,
            liveKey: liveIATAKey,
            liveSeats: liveSeats,
            seatDelta: seatDelta
        )

        print("[EquipmentSwap] \(summary)")

        return EquipmentSwapResult(
            scheduledName: scheduledName,
            scheduledSeats: scheduledSeats,
            liveName: liveName,
            liveSeats: liveSeats,
            seatDelta: seatDelta,
            severity: severity,
            summary: summary
        )
    }

    // MARK: - Loading

    private func loadIfNeeded() {
        if didAttemptLoad { return }
        didAttemptLoad = true

        guard let url = Bundle.main.url(forResource: "aircraft_seats", withExtension: "json") else {
            print("[EquipmentSwap] aircraft_seats.json not found in bundle")
            return
        }
        do {
            let data = try Data(contentsOf: url)
            catalog = try JSONDecoder().decode(Catalog.self, from: data)
            print("[EquipmentSwap] loaded catalog: \(catalog?.iata.count ?? 0) IATA / \(catalog?.icao.count ?? 0) ICAO entries")
        } catch {
            print("[EquipmentSwap] decode failed: \(error)")
            Task { @MainActor in
                DataIntegrityMonitor.shared.report("aircraft_seats.json", error: error)
            }
        }
    }

    // MARK: - Helpers

    private static func iataKey(forICAO icao: String?, catalog: Catalog) -> String? {
        guard let raw = icao?.uppercased(), !raw.isEmpty else { return nil }
        if let mapped = catalog.icao[raw] { return mapped }
        // ICAO occasionally matches an IATA verbatim (rare). Allow that fallback.
        if catalog.iata[raw] != nil { return raw }
        return nil
    }

    private static func summary(
        scheduledKey: String,
        scheduledSeats: Int,
        liveKey: String?,
        liveSeats: Int?,
        seatDelta: Int?
    ) -> String {
        guard let liveKey, let liveSeats, let seatDelta else {
            return "Scheduled: \(scheduledKey) (\(scheduledSeats) seats) — live equipment unknown"
        }

        if seatDelta == 0 {
            return "Same equipment today: \(scheduledKey) (\(scheduledSeats))"
        }

        let prefix = seatDelta < 0 ? "Smaller bird today" : "Bigger bird today"
        let magnitude = abs(seatDelta)
        let direction = seatDelta < 0 ? "fewer" : "more"
        return "\(prefix): \(scheduledKey) (\(scheduledSeats)) vs \(liveKey) (\(liveSeats)) — \(magnitude) \(direction) seats"
    }
}
