import Foundation

/// Predicted per-flight load factor (fraction of seats expected to be
/// occupied) for a specific carrier/flight/route/date combination.
///
/// This is the key signal for an airline-employee / nonrev traveller —
/// "is this flight likely to be wide open or stuffed?". The actor blends
/// BTS historical baselines with calendar adjustments (weekday-vs-weekend,
/// peak-season bumps) and an optional live-seat correction.
///
/// All math is intentionally simple and explainable: the basis string we
/// return is what the UI surfaces to the user, so they understand *why*
/// the prediction is what it is.
actor LoadFactorService {

    // MARK: Singleton

    static let shared = LoadFactorService()

    // MARK: Dependencies

    private let store: BTSDataStore

    init(store: BTSDataStore = .shared) {
        self.store = store
    }

    // MARK: Public API

    /// Predict the load factor for a given flight on a given date.
    ///
    /// - Parameters:
    ///   - carrier:      IATA carrier code (e.g. "WN").
    ///   - flightNumber: Operating flight number.
    ///   - origin:       Origin IATA.
    ///   - dest:         Destination IATA.
    ///   - date:         Departure date — used for day-of-week + seasonal adjustments.
    ///   - liveSeats:    Optional live seat count from the actually-assigned
    ///                   aircraft (e.g. parsed from FR24 / AirframeMetadata).
    ///                   If smaller than the BTS historical seat count, the
    ///                   predicted load factor scales up proportionally
    ///                   because the same expected pax count fills more of
    ///                   a smaller jet.
    /// - Returns: ``nil`` when there's no BTS record for the flight key —
    ///   callers should hide the load-factor UI rather than guess.
    ///
    /// - Note: When `database` is provided we look up the origin airport's
    ///   timezone so weekday + month adjustments are evaluated in
    ///   airport-local time. Callers that don't have a database (or that
    ///   want the legacy UTC behaviour) can leave it nil.
    func estimate(
        carrier: String,
        flightNumber: Int,
        origin: String,
        dest: String,
        date: Date,
        database: AirportDatabase? = nil,
        liveSeats: Int? = nil
    ) async -> LoadFactorEstimate? {
        guard let base = await store.record(
            carrier: carrier,
            flightNumber: flightNumber,
            origin: origin,
            dest: dest
        ) else {
            print("[LoadFactor] no BTS record for \(carrier)\(flightNumber) \(origin)->\(dest)")
            return nil
        }

        var prediction = base.avgLoadFactor
        var reasons: [String] = ["BTS avg \(Int(round(base.avgLoadFactor * 100)))% (\(base.samplePeriod))"]

        // ---- Day-of-week adjustment ------------------------------------
        // Calendar uses 1=Sunday ... 7=Saturday. Weekend = Sat or Sun.
        // We resolve the origin airport's timezone so the weekday/month
        // reflect what a passenger would actually call "Sunday" — late-PT
        // departures otherwise roll into Monday UTC and skip the bump.
        let originTimeZone: TimeZone = database?.timeZone(forIATA: origin)
            ?? TimeZone(identifier: "UTC")
            ?? .current
        var cal = Calendar(identifier: .gregorian)
        cal.timeZone = originTimeZone
        let weekday = cal.component(.weekday, from: date)
        let isWeekend = (weekday == 1 || weekday == 7)

        if isWeekend {
            if Self.leisureCarriers.contains(carrier.uppercased()) {
                prediction += 0.05
                reasons.append("weekend leisure +5%")
            } else if Self.businessCarriers.contains(carrier.uppercased()) {
                prediction -= 0.05
                reasons.append("weekend business -5%")
            }
        }

        // ---- Peak-season adjustment ------------------------------------
        let month = cal.component(.month, from: date)
        if month == 6 || month == 7 || month == 12 {
            prediction += 0.07
            reasons.append("peak season +7%")
        }

        // ---- Live-seats correction -------------------------------------
        // If today's airframe seats < historical avg seats, the same pax
        // demand fills a larger fraction of the cabin. Scale by the seat
        // ratio. Guards (all paths leave `prediction` untouched):
        //   • liveSeats <= 0       — missing/garbage live data.
        //   • base.avgSeats <= 0   — guards against future bad BTS records
        //                            (divide-by-zero hazard otherwise).
        //   • liveSeats >= avgSeats — same-size or bigger airframe; the
        //                            ratio path is only meant to push the
        //                            number *up*, never down.
        if let liveSeats,
           liveSeats > 0,
           base.avgSeats > 0,
           liveSeats < base.avgSeats {
            let ratio = Double(base.avgSeats) / Double(liveSeats)
            let bumped = prediction * ratio
            if bumped > prediction {
                let added = bumped - prediction
                prediction = bumped
                reasons.append(String(
                    format: "smaller aircraft (%d vs %d seats) +%d%%",
                    liveSeats, base.avgSeats, Int(round(added * 100))
                ))
            }
        }

        // Clamp to [0, 1] — adjustments can push us over.
        prediction = min(1.0, max(0.0, prediction))

        // ---- Confidence ------------------------------------------------
        let confidence: Double
        if base.totalFlights >= 60 {
            confidence = 0.85
        } else if base.totalFlights >= 20 {
            confidence = 0.65
        } else {
            confidence = 0.40
        }

        let basis = reasons.joined(separator: " · ")
        print("[LoadFactor] \(carrier)\(flightNumber) \(origin)->\(dest) " +
              "predicted=\(Int(round(prediction * 100)))% conf=\(confidence) \(basis)")

        return LoadFactorEstimate(
            predicted: prediction,
            confidence: confidence,
            baseSeats: liveSeats ?? base.avgSeats,
            basis: basis
        )
    }

    // MARK: Carrier classification

    /// Carriers we treat as "leisure" — weekend traffic skews higher.
    private static let leisureCarriers: Set<String> = ["WN", "NK", "F9", "G4", "SY"]

    /// Carriers we treat as "business" — weekend traffic skews lower,
    /// because Mon/Thu corporate trips dominate the schedule.
    private static let businessCarriers: Set<String> = ["AA", "DL", "UA"]
}

// MARK: - Estimate type

/// Result of a load-factor prediction call. ``predicted`` and
/// ``confidence`` are both in 0...1, intended for direct rendering
/// (e.g. a colour gauge plus a small confidence pill).
struct LoadFactorEstimate: Sendable {
    /// Predicted load factor (0...1).
    let predicted: Double

    /// Confidence in the prediction (0...1) — derived from sample size.
    let confidence: Double

    /// Seat count used as the denominator in the prediction. This is
    /// either the live aircraft's seat count (if provided) or the BTS
    /// historical average.
    let baseSeats: Int

    /// Human-readable explanation of what fed into the prediction.
    /// Designed to be surfaced verbatim in the UI ("Why this number?").
    let basis: String
}
