import Foundation

/// The slice of `AircraftRotationTracker` the cascade predictor consumes.
/// Lets the Phase-1 cascade tests inject a deterministic rotation history
/// without standing up a real OpenSky client.
protocol AircraftRotationProvider: Sendable {
    func rotation(forICAO24 icao24: String, lookbackHours: Int) async -> [AircraftRotationTracker.RotationSegment]
}

extension AircraftRotationTracker: AircraftRotationProvider {}

/// Predicts downstream delay propagation for a scheduled flight by looking
/// at the operating aircraft's most recent rotation segment. The model is
/// intentionally simple — narrowbody turns absorb ~45 minutes of upstream
/// late-arrival before they push the downstream block time.
actor DelayCascadePredictor {

    static let shared = DelayCascadePredictor()

    struct CascadePrediction: Sendable {
        let confidence: Double
        let predictedDelayMin: Int
        let basis: String
        let upstreamSegment: AircraftRotationTracker.RotationSegment?
    }

    private let tracker: AircraftRotationProvider

    /// Minimum turn time we credit a narrowbody (737/A320 family) with.
    /// Anything less than this on the upstream delay is absorbed by the
    /// scheduled ground time and won't cascade.
    private static let narrowbodyTurnMinutes = 45

    /// We only report a propagated delay if the upstream segment landed
    /// at least this many minutes after the downstream's scheduled
    /// departure (or close to it). Below this threshold a quick turn
    /// is realistic.
    private static let upstreamLateThresholdMinutes = 15

    init(tracker: AircraftRotationProvider = AircraftRotationTracker()) {
        self.tracker = tracker
    }

    /// Predict downstream delay. Returns nil when we can't make a
    /// meaningful prediction — no aircraft, no rotation data, or the
    /// aircraft isn't actually positioned to operate this flight.
    func predict(carrier: String,
                 flightNumber: Int,
                 scheduledDeparture: Date,
                 departureICAO: String,
                 operatingICAO24: String?) async -> CascadePrediction? {
        guard let icao24 = operatingICAO24?.trimmingCharacters(in: .whitespacesAndNewlines),
              !icao24.isEmpty else {
            print("[DelayCascade] no aircraft assigned — skipping prediction")
            return nil
        }

        let rotation = await tracker.rotation(forICAO24: icao24, lookbackHours: 18)
        guard let lastSegment = rotation.last else {
            print("[DelayCascade] no rotation history for icao24=\(icao24)")
            return nil
        }

        let normalizedScheduledStation = Self.normalizeStation(departureICAO)
        let normalizedSegmentArrival = Self.normalizeStation(lastSegment.arrivalICAO ?? "")

        // If the aircraft's last leg didn't land at our departure
        // station, this rotation isn't relevant. (Either we have the
        // wrong tail or the aircraft is still mid-rotation.)
        // Comparison is form-agnostic: a 3-letter IATA on one side and
        // a 4-letter ICAO for the same airport on the other compare
        // equal — see `stationsMatch` for the matrix.
        guard !normalizedSegmentArrival.isEmpty,
              Self.stationsMatch(normalizedScheduledStation, normalizedSegmentArrival) else {
            print("[DelayCascade] last segment arrived at \(normalizedSegmentArrival.isEmpty ? "?" : normalizedSegmentArrival), need \(normalizedScheduledStation) — no prediction")
            return nil
        }

        // Compute upstream lateness against the downstream's scheduled
        // departure. If the aircraft arrived early or on time relative
        // to scheduled departure, the turn will absorb everything.
        let lateMinutes = Int((lastSegment.arrivalTime.timeIntervalSince(scheduledDeparture) / 60.0).rounded())
        let upstreamDelay = max(0, lateMinutes + Self.narrowbodyTurnMinutes)
        // upstreamDelay here is "how late after touchdown the aircraft
        // must depart": touchdown + 45min minimum turn. If
        // scheduledDeparture is later than that, no cascade.
        _ = upstreamDelay

        let earliestPushback = lastSegment.arrivalTime.addingTimeInterval(Double(Self.narrowbodyTurnMinutes) * 60)
        let propagatedMinutes = Int((earliestPushback.timeIntervalSince(scheduledDeparture) / 60.0).rounded())

        guard propagatedMinutes > 0 else {
            print("[DelayCascade] turn absorbs upstream — earliest pushback \(earliestPushback) vs scheduled \(scheduledDeparture)")
            return nil
        }

        // Also gate on the raw upstream lateness; a 5-minute late
        // arrival isn't worth surfacing as a cascade.
        guard lateMinutes >= Self.upstreamLateThresholdMinutes ||
              propagatedMinutes >= Self.upstreamLateThresholdMinutes else {
            print("[DelayCascade] upstream only \(lateMinutes)min late — below threshold")
            return nil
        }

        let confidence = Self.confidence(propagatedMinutes: propagatedMinutes, lateMinutes: lateMinutes)
        let basis = Self.basisString(
            icao24: icao24,
            lateMinutes: max(lateMinutes, propagatedMinutes),
            upstreamFromICAO: lastSegment.departureICAO
        )

        print("[DelayCascade] \(carrier)\(flightNumber) at \(normalizedScheduledStation): +\(propagatedMinutes)min cascade (\(basis))")

        return CascadePrediction(
            confidence: confidence,
            predictedDelayMin: propagatedMinutes,
            basis: basis,
            upstreamSegment: lastSegment
        )
    }

    // MARK: - Helpers

    private static func basisString(icao24: String, lateMinutes: Int, upstreamFromICAO: String?) -> String {
        let tail = icao24.uppercased()
        if let from = upstreamFromICAO?.uppercased(), !from.isEmpty {
            return "Aircraft \(tail) landed \(lateMinutes)min late from \(from)"
        }
        return "Aircraft \(tail) landed \(lateMinutes)min late"
    }

    /// Trim + uppercase. Returned form is whatever the caller passed
    /// us — we don't try to map IATA↔ICAO here, `stationsMatch` does that.
    private static func normalizeStation(_ raw: String) -> String {
        raw.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
    }

    /// Compare two airport codes that may be in different forms (IATA
    /// 3-letter vs ICAO 4-letter). Returns true when both forms refer to
    /// the same airport.
    ///
    /// We accept the following equivalences for US/Canada/Mexico (where
    /// IATA→ICAO is a deterministic prefix transform):
    ///   - "KDFW" == "DFW"   (US: drop leading K)
    ///   - "CYYZ" == "YYZ"   (Canada: drop leading C)
    ///   - "MMMX" == "MMX"   (Mexico: drop leading M) — rare but covered.
    /// Outside those regions we fall back to exact equality on the
    /// uppercased form, which is the right answer for ICAO↔ICAO and
    /// IATA↔IATA comparisons.
    private static func stationsMatch(_ a: String, _ b: String) -> Bool {
        if a == b { return true }
        let aShort = shortFormICAO(a)
        let bShort = shortFormICAO(b)
        return aShort == bShort
    }

    /// Map a 4-letter ICAO to its 3-letter IATA equivalent for the
    /// regions where the mapping is a simple prefix drop. Returns the
    /// input unchanged otherwise.
    private static func shortFormICAO(_ code: String) -> String {
        guard code.count == 4 else { return code }
        if code.hasPrefix("K") { return String(code.dropFirst()) }
        if code.hasPrefix("CY") { return String(code.dropFirst()) }
        if code.hasPrefix("MM") { return String(code.dropFirst()) }
        return code
    }

    /// Confidence rises with both the upstream lateness and the
    /// propagated delay magnitude — a 60-minute late upstream
    /// arrival is a strong signal, a borderline 16-minute one less so.
    private static func confidence(propagatedMinutes: Int, lateMinutes: Int) -> Double {
        let signal = Double(max(propagatedMinutes, lateMinutes))
        // Map 15min → 0.5, 60min → 0.9, 120min+ → 0.95
        let normalized = min(1.0, signal / 120.0)
        let scaled = 0.4 + 0.55 * normalized
        return (scaled * 100).rounded() / 100
    }
}
