import Foundation
import CoreLocation

/// Reconstructs an aircraft's recent rotation (sequence of flights) from
/// OpenSky data so we can reason about how upstream delays will cascade
/// into a downstream segment.
///
/// We prefer OpenSky's `/flights/aircraft` history endpoint — it already
/// segments by takeoff/landing and tags each leg with the operating
/// airport ICAO. When that endpoint returns nothing usable (common for
/// recent activity inside the last hour or two), we fall back to the
/// `/tracks/all` path and synthesize segments by walking the
/// `onGround` flag in the track points.
actor AircraftRotationTracker {
    /// Shared instance so per-tap detail sheets reuse the same OpenSky
    /// client (which has its own rate-limit accounting) and the actor's
    /// own cache — instead of paying for a fresh AirportDatabase load on
    /// every aircraft tap.
    static let shared = AircraftRotationTracker()

    struct RotationSegment: Sendable, Identifiable {
        let id: String
        let departureICAO: String?
        let arrivalICAO: String?
        let departureTime: Date
        let arrivalTime: Date
        let estimatedDelayMin: Int?
    }

    private let client: OpenSkyClient
    private let airports: AirportDatabase

    init(client: OpenSkyClient = OpenSkyClient(),
         airports: AirportDatabase = AirportDatabase()) {
        self.client = client
        self.airports = airports
    }

    /// Returns the aircraft's recent flight segments, ordered oldest →
    /// newest. Empty if OpenSky has no usable data for the lookback
    /// window.
    func rotation(forICAO24 icao24: String, lookbackHours: Int = 18) async -> [RotationSegment] {
        let trimmed = icao24.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        guard !trimmed.isEmpty else {
            print("[RotationTracker] empty icao24")
            return []
        }

        let now = Date()
        let cutoff = now.addingTimeInterval(-Double(lookbackHours) * 3600)

        // Strategy 1: OpenSky's flights/aircraft endpoint. It needs a day
        // window — request enough days to cover lookbackHours. The
        // endpoint caps each call at 30 days; we never need more than 2.
        let daysBack = max(1, Int(ceil(Double(lookbackHours) / 24.0)))
        let flights = await client.recentFlights(icao24: trimmed, daysBack: daysBack)

        let usable = flights
            .filter { $0.arrivalDate >= cutoff }
            .sorted { $0.firstSeen < $1.firstSeen }

        if !usable.isEmpty {
            print("[RotationTracker] icao24=\(trimmed) lookback=\(lookbackHours)h → \(usable.count) flight(s) from recentFlights")
            return usable.map { Self.segment(from: $0) }
        }

        // Strategy 2: fall back to the live track and walk the
        // onGround flag. This catches very-recent activity that
        // hasn't yet been written to OpenSky's flights index.
        if let track = await client.track(icao24: trimmed) {
            let synthesized = Self.segments(from: track, airports: airports, since: cutoff)
            print("[RotationTracker] icao24=\(trimmed) lookback=\(lookbackHours)h → \(synthesized.count) synthesized segment(s) from track")
            return synthesized
        }

        print("[RotationTracker] icao24=\(trimmed) lookback=\(lookbackHours)h → no data")
        return []
    }

    // MARK: - Helpers

    private static func segment(from flight: OpenSkyFlight) -> RotationSegment {
        // OpenSky doesn't supply a scheduled time, so we leave estimated
        // delay nil here; the cascade predictor compares actual arrival
        // against the next leg's scheduled departure instead.
        let dep = flight.estDepartureAirport?.uppercased()
        let arr = flight.estArrivalAirport?.uppercased()
        let id = "\(flight.icao24)-\(flight.firstSeen)"
        return RotationSegment(
            id: id,
            departureICAO: (dep?.isEmpty == false) ? dep : nil,
            arrivalICAO: (arr?.isEmpty == false) ? arr : nil,
            departureTime: flight.departureDate,
            arrivalTime: flight.arrivalDate,
            estimatedDelayMin: nil
        )
    }

    /// Walks the track's path entries and groups contiguous airborne
    /// runs into segments. A segment is bounded by:
    /// - takeoff: transition from onGround=true → onGround=false
    /// - landing: transition from onGround=false → onGround=true
    /// The endpoints' lat/lon are mapped to the nearest airport (within
    /// a generous radius — taxiways can be a few miles from the field
    /// center) for the ICAO field; we only have IATA in the bundled DB,
    /// so the stored string is the IATA code when sourced from track.
    private static func segments(from track: AircraftTrack,
                                 airports: AirportDatabase,
                                 since cutoff: Date) -> [RotationSegment] {
        guard !track.path.isEmpty else { return [] }
        // Path entries are time-ordered ascending per OpenSky's contract.
        let path = track.path

        var segments: [RotationSegment] = []
        var airborneStart: AircraftTrack.TrackPoint?
        var lastAirborne: AircraftTrack.TrackPoint?

        // Track the ground point immediately preceding the current
        // airborne run so we can read the departure fix from it (more
        // accurate than the first airborne sample, which is already
        // a few seconds airborne).
        var lastGround: AircraftTrack.TrackPoint?

        for point in path {
            if point.onGround {
                if let start = airborneStart, let end = lastAirborne ?? lastGround {
                    // We just landed; close the segment.
                    let depPoint = lastGround ?? start
                    let seg = makeSegment(
                        icao24: track.icao24,
                        depPoint: depPoint,
                        arrPoint: point,
                        airborneStart: start,
                        airborneEnd: end,
                        airports: airports
                    )
                    if seg.arrivalTime >= cutoff {
                        segments.append(seg)
                    }
                    airborneStart = nil
                    lastAirborne = nil
                }
                lastGround = point
            } else {
                if airborneStart == nil {
                    airborneStart = point
                }
                lastAirborne = point
            }
        }

        // If the aircraft is still airborne at the end of the track,
        // emit a partial segment so callers can see where it's coming
        // from. arrivalTime is the last position fix.
        if let start = airborneStart, let end = lastAirborne {
            let depPoint = lastGround ?? start
            let seg = makeSegment(
                icao24: track.icao24,
                depPoint: depPoint,
                arrPoint: end,
                airborneStart: start,
                airborneEnd: end,
                airports: airports
            )
            if seg.arrivalTime >= cutoff {
                segments.append(seg)
            }
        }

        return segments
    }

    private static func makeSegment(icao24: String,
                                    depPoint: AircraftTrack.TrackPoint,
                                    arrPoint: AircraftTrack.TrackPoint,
                                    airborneStart: AircraftTrack.TrackPoint,
                                    airborneEnd: AircraftTrack.TrackPoint,
                                    airports: AirportDatabase) -> RotationSegment {
        let depCoord = CLLocationCoordinate2D(latitude: depPoint.latitude, longitude: depPoint.longitude)
        let arrCoord = CLLocationCoordinate2D(latitude: arrPoint.latitude, longitude: arrPoint.longitude)
        let depAirport = airports.nearestAirport(to: depCoord, maxMiles: 10)
        let arrAirport = airports.nearestAirport(to: arrCoord, maxMiles: 10)
        return RotationSegment(
            id: "\(icao24)-\(airborneStart.time)",
            departureICAO: depAirport?.iata,
            arrivalICAO: arrAirport?.iata,
            departureTime: Date(timeIntervalSince1970: TimeInterval(airborneStart.time)),
            arrivalTime: Date(timeIntervalSince1970: TimeInterval(airborneEnd.time)),
            estimatedDelayMin: nil
        )
    }
}
