import SwiftUI
import SwiftData
import CoreLocation

struct LiveFlightDetailSheet: View {
    let aircraft: LiveAircraft
    let openSky: OpenSkyClient
    let routeExplorer: RouteExplorerClient
    let database: AirportDatabase

    @State private var recentFlights: [OpenSkyFlight] = []
    @State private var isLoadingRoute = false
    @State private var aircraftPhoto: AircraftPhotoService.Photo?
    @State private var showingAddToHistory = false

    // Phase-2 enrichment state. Each card has its own optional so it can
    // independently render an empty state (or hide itself entirely) without
    // blocking the rest of the sheet. All loaded inside `loadEnrichments()`
    // which fires once the route has been resolved.
    @State private var loadFactor: LoadFactorEstimate?
    @State private var onTimeStat: OnTimeStat?
    @State private var equipmentSwap: EquipmentSwapService.EquipmentSwapResult?
    @State private var originWeather: WeatherForecast?
    @State private var arrivalWeather: WeatherForecast?
    @State private var cascade: DelayCascadePredictor.CascadePrediction?
    @State private var sisters: [SisterFlightService.SisterFlight] = []
    @State private var btsMetadata: BTSMetadata?
    /// Most recent rotation segment for the aircraft operating this
    /// flight. Used to drive an "Aircraft status" card when we don't
    /// have a scheduled departure to compare against (the FR24 live
    /// path — the only live path now that route-explorer is broken).
    @State private var aircraftStatus: AircraftRotationTracker.RotationSegment?

    /// The resolved route for the current selection. Built from a cascade:
    /// scheduled flight (via route-explorer) → OpenSky history → trail-based
    /// nearest-airport inference. See `resolveRoute()`.
    @State private var resolvedRoute: ResolvedRoute?

    enum ResolvedRoute {
        /// Inline data straight off the FR24 feed (departure + arrival
        /// IATA, plus the flight number). This is the fast path — no
        /// extra network call required because FR24 already gave us
        /// these in the feed.js response.
        case fromFR24(departureIATA: String?, arrivalIATA: String?, flightIATA: String?)
        /// Real schedule match from route-explorer. Best fidelity for
        /// flights that aren't FR24-sourced.
        case scheduled(RouteFlight)
        /// OpenSky historical flight. departure + arrival from FAA tracking.
        case fromOpenSky(OpenSkyFlight, ageHours: Int)
        /// Couldn't find a flight record — inferred departure from the
        /// aircraft's trail start. Arrival unknown.
        case inferred(departureIATA: String, departureName: String?)
    }

    @Environment(\.dismiss) private var dismiss
    @Environment(\.openURL) private var openURL
    @Environment(\.modelContext) private var modelContext

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(alignment: .leading, spacing: 16) {
                    header

                    // Depart → Arrival sits directly under the callsign
                    // header — it's the single most important thing the
                    // user opened the sheet to see.
                    routeSection

                    addToHistoryButton

                    // Aircraft photo follows the route. Negative
                    // horizontal padding lets the photo break out of the
                    // 16pt content padding to be full-bleed edge-to-edge.
                    photoBanner
                        .padding(.horizontal, -16)

                    if let photo = aircraftPhoto, let credit = photo.photographer {
                        photoCredit(name: credit, link: photo.detailLink)
                    }

                    Divider()

                    Text("LIVE STATE")
                        .font(FlightTheme.label())
                        .foregroundStyle(FlightTheme.textTertiary)
                        .tracking(1)

                    liveStateGrid

                    if recentFlights.count > 1 {
                        Text("RECENT FLIGHTS")
                            .font(FlightTheme.label())
                            .foregroundStyle(FlightTheme.textTertiary)
                            .tracking(1)
                            .padding(.top, 4)

                        ForEach(recentFlights.prefix(8), id: \.self) { flight in
                            recentFlightRow(flight)
                        }
                    }

                    Text("AIRCRAFT")
                        .font(FlightTheme.label())
                        .foregroundStyle(FlightTheme.textTertiary)
                        .tracking(1)
                        .padding(.top, 4)

                    aircraftCard

                    enrichmentSections
                }
                .padding(16)
            }
            .background(FlightTheme.background.ignoresSafeArea())
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button { dismiss() } label: {
                        Image(systemName: "xmark")
                            .font(.subheadline.weight(.semibold))
                    }
                }
            }
            .task {
                await resolveRoute()
            }
            .task(id: aircraft.icao24) {
                aircraftPhoto = await AircraftPhotoService.shared.photo(
                    registration: aircraft.enrichment?.registration,
                    icao24: aircraft.icao24
                )
            }
            .task(id: enrichmentTaskID) {
                await loadEnrichments()
            }
            // Cascade tail refreshes every 60s while the sheet is open.
            // SwiftUI auto-cancels this task when the view disappears or
            // when the id changes (i.e. the user opens a different
            // aircraft), so we never leak the timer.
            .task(id: aircraft.icao24) {
                while !Task.isCancelled {
                    try? await Task.sleep(for: .seconds(60))
                    await refreshCascadeOnly()
                }
            }
        }
    }

    /// Re-key the enrichment loader once the route resolution flips between
    /// cases. Without this id, the task would fire before `resolvedRoute`
    /// is populated and we'd never get origin/dest IATAs.
    private var enrichmentTaskID: String {
        let ctx = flightContext
        return [
            aircraft.icao24,
            ctx?.carrierIATA ?? "-",
            String(ctx?.flightNumber ?? -1),
            ctx?.originIATA ?? "-",
            ctx?.destIATA ?? "-"
        ].joined(separator: "|")
    }

    // MARK: - Route resolution

    /// Cascade to find departure + arrival, in order of fidelity:
    ///   1. Scheduled lookup via route-explorer (carrier + flight number)
    ///      — this is the only path that gives a real arrival airport for
    ///      an in-progress flight.
    ///   2. OpenSky `/flights/aircraft` — only landed flights; useful if
    ///      the aircraft just landed or for "last flight" context.
    ///   3. Trail-based inference — first point of the OpenSky `/tracks/all`
    ///      response → nearest airport in our local DB → "Departed from X".
    private func resolveRoute() async {
        isLoadingRoute = true
        defer { isLoadingRoute = false }

        // 0) Fast path: FR24's feed gave us departure + arrival inline.
        //    This is the common case now that FR24 is the primary feed.
        //    Skips the route-explorer network call entirely.
        if let e = aircraft.enrichment,
           e.departureIATA != nil || e.arrivalIATA != nil {
            resolvedRoute = .fromFR24(
                departureIATA: e.departureIATA,
                arrivalIATA: e.arrivalIATA,
                flightIATA: e.flightIATA
            )
            // Fire OpenSky history in the background for "recent flights".
            recentFlights = await openSky.recentFlights(icao24: aircraft.icao24)
            return
        }

        // 1) Try the scheduled lookup if we have a parseable airline + flight.
        if let scheduled = await tryScheduledLookup() {
            resolvedRoute = .scheduled(scheduled)
            // Still fetch OpenSky history in the background for the "recent
            // flights" list — but we don't need to await it.
            recentFlights = await openSky.recentFlights(icao24: aircraft.icao24)
            return
        }

        // 2) Fall back to OpenSky history.
        let flights = await openSky.recentFlights(icao24: aircraft.icao24)
        recentFlights = flights
        if let mostRecent = flights.first {
            let hoursAgo = max(0, Int(
                (Date().timeIntervalSince1970 - Double(mostRecent.lastSeen)) / 3600
            ))
            resolvedRoute = .fromOpenSky(mostRecent, ageHours: hoursAgo)
            return
        }

        // 3) Last resort: nearest-airport from the trail start.
        if let track = await openSky.track(icao24: aircraft.icao24),
           let firstPoint = track.path.first {
            let coord = CLLocationCoordinate2D(
                latitude: firstPoint.latitude,
                longitude: firstPoint.longitude
            )
            if let nearest = database.nearestAirport(to: coord, maxMiles: 30) {
                resolvedRoute = .inferred(
                    departureIATA: nearest.iata,
                    departureName: nearest.name
                )
                return
            }
        }
        resolvedRoute = nil
    }

    /// Parse the ADS-B callsign (e.g. "AAL1234") into carrier + flight number
    /// and hit route-explorer's `/schedule` endpoint. Returns the day's
    /// operating record if route-explorer indexes the carrier; nil otherwise.
    private func tryScheduledLookup() async -> RouteFlight? {
        guard let icao = aircraft.airlineICAO,
              let airline = AircraftRegistry.shared.lookup(icao: icao),
              let iata = airline.iata,
              let flightStr = aircraft.flightNumber,
              let flightNum = Int(flightStr)
        else { return nil }

        // Look across today ± yesterday — handles late-running redeyes that
        // departed on D-1 but are still airborne on D.
        let today = Date()
        let yesterday = today.addingTimeInterval(-86400)
        let results = await routeExplorer.searchSchedule(
            carrierCode: iata,
            flightNumber: flightNum,
            startDate: yesterday,
            endDate: today
        )
        guard !results.isEmpty else { return nil }

        // Pick the entry whose departure is closest to "now" (typically
        // the in-progress flight).
        return results.min { a, b in
            abs(a.departure.dateTime.timeIntervalSinceNow) <
            abs(b.departure.dateTime.timeIntervalSinceNow)
        }
    }

    // MARK: - Add-to-history button
    //
    // Lives between the route card and the photo banner — the natural
    // "I'm on this plane right now, save it" spot. Opens AddFlightView
    // pre-populated from FR24 enrichment so the user can confirm any
    // detail before it lands in their log.

    private var addToHistoryButton: some View {
        Button {
            showingAddToHistory = true
        } label: {
            HStack(spacing: 8) {
                Image(systemName: "plus.circle.fill")
                Text("Add to my flights")
                    .font(.subheadline.weight(.semibold))
            }
            .frame(maxWidth: .infinity)
            .padding(.vertical, 12)
        }
        .background(FlightTheme.accent, in: RoundedRectangle(cornerRadius: 12))
        .foregroundStyle(.white)
        .sheet(isPresented: $showingAddToHistory) {
            let store = FlightHistoryStore(context: modelContext, airportDatabase: database)
            AddFlightView(
                routeExplorer: routeExplorer,
                database: database,
                store: store,
                prefill: AddFlightView.Prefill(
                    flightDate: Date(),
                    carrierICAO: aircraft.airlineICAO,
                    carrierIATA: AircraftRegistry.shared.lookup(icao: aircraft.airlineICAO)?.iata,
                    flightNumber: aircraft.flightNumber,
                    departureIATA: aircraft.enrichment?.departureIATA,
                    arrivalIATA: aircraft.enrichment?.arrivalIATA,
                    scheduledDeparture: nil,
                    scheduledArrival: nil,
                    aircraftType: aircraft.typeCode,
                    registration: aircraft.enrichment?.registration,
                    icao24: aircraft.icao24,
                    source: "live-tap"
                )
            )
        }
    }

    // MARK: - Photo banner
    //
    // Hero image at the very top of the sheet, sourced from planespotters.
    // Hides itself entirely when no photo is available (lots of GA / cargo
    // / older airframes have no public photo). Most recent photo per
    // airframe is what planespotters serves — which means special
    // liveries surface naturally because photographers chase them first.

    @ViewBuilder
    private var photoBanner: some View {
        if let photo = aircraftPhoto {
            AsyncImage(url: photo.largeURL) { phase in
                switch phase {
                case .success(let img):
                    img.resizable().aspectRatio(contentMode: .fill)
                case .empty, .failure:
                    Rectangle().fill(FlightTheme.cardBackground)
                @unknown default:
                    Rectangle().fill(FlightTheme.cardBackground)
                }
            }
            .frame(maxWidth: .infinity)
            .frame(height: 200)
            .clipped()
        }
    }

    private func photoCredit(name: String, link: URL?) -> some View {
        HStack(spacing: 4) {
            Image(systemName: "camera.fill")
                .font(.caption2)
            Text("Photo by \(name) · planespotters.net")
                .font(.caption2)
        }
        .foregroundStyle(FlightTheme.textTertiary)
        .padding(.top, 4)
        .contentShape(Rectangle())
        .onTapGesture {
            if let link { openURL(link) }
        }
    }

    // MARK: - Header

    private var header: some View {
        VStack(alignment: .leading, spacing: 4) {
            HStack(spacing: 10) {
                if let logoURL = airlineEntry?.logoURL {
                    AsyncImage(url: logoURL) { phase in
                        switch phase {
                        case .success(let img):
                            img.resizable().scaledToFit()
                        default:
                            RoundedRectangle(cornerRadius: 8)
                                .fill(FlightTheme.accent.opacity(0.2))
                        }
                    }
                    .frame(width: 40, height: 40)
                    .clipShape(RoundedRectangle(cornerRadius: 8))
                }
                Text(aircraft.trimmedCallsign ?? aircraft.icao24)
                    .font(.title.weight(.bold).monospaced())
                    .foregroundStyle(FlightTheme.textPrimary)
                Spacer()
                statusBadge
            }
            Text(airlineDisplayName)
                .font(.subheadline)
                .foregroundStyle(FlightTheme.textSecondary)
        }
    }

    private var statusBadge: some View {
        let (text, color): (String, Color) = {
            if aircraft.onGround { return ("On ground", FlightTheme.textSecondary) }
            switch aircraft.verticalState {
            case .climbing:   return ("Climbing",   FlightTheme.onTime)
            case .descending: return ("Descending", FlightTheme.delayed)
            case .level:      return ("Cruising",   FlightTheme.accent)
            }
        }()
        return Text(text)
            .font(.caption.weight(.bold))
            .foregroundStyle(.white)
            .padding(.horizontal, 10)
            .padding(.vertical, 4)
            .background(color, in: Capsule())
    }

    // MARK: - Live state grid

    private var liveStateGrid: some View {
        VStack(spacing: 0) {
            HStack(spacing: 0) {
                statCell(label: "Altitude",
                         value: aircraft.altitudeFeet.map { "\(formatNumber($0)) ft" } ?? "—")
                statCell(label: "Speed",
                         value: aircraft.velocityKnots.map { "\($0) kt" } ?? "—")
            }
            Divider()
            HStack(spacing: 0) {
                statCell(label: "Heading",
                         value: aircraft.heading.map { "\($0)°" } ?? "—")
                statCell(label: "Vertical",
                         value: verticalDisplay)
            }
            Divider()
            HStack(spacing: 0) {
                statCell(label: "Squawk", value: aircraft.squawk ?? "—")
                statCell(label: "Updated", value: shortTime(aircraft.lastContact))
            }
        }
        .flightCard(padding: 0)
    }

    private var verticalDisplay: String {
        if aircraft.onGround { return "—" }
        switch aircraft.verticalState {
        case .climbing:   return "Climb"
        case .descending: return "Descend"
        case .level:      return "Level"
        }
    }

    private func statCell(label: String, value: String) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(label)
                .font(.caption2)
                .foregroundStyle(FlightTheme.textTertiary)
                .tracking(0.5)
            Text(value)
                .font(.subheadline.weight(.semibold).monospaced())
                .foregroundStyle(FlightTheme.textPrimary)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(16)
    }

    // MARK: - Route
    //
    // OpenSky's /flights/aircraft endpoint only returns *landed* flights, so
    // for a plane currently in flight we usually get nothing relevant — the
    // most recent entry would be its previous leg (often from yesterday).
    //
    // To still show something useful, we display whatever's most recent and
    // label it clearly: "In flight" if the snapshot is fresh, "Last flight"
    // if it's older, "—" only when we genuinely have nothing.

    @ViewBuilder
    private var routeSection: some View {
        if let resolved = resolvedRoute {
            VStack(alignment: .leading, spacing: 6) {
                HStack(spacing: 8) {
                    Text(headerLabel(for: resolved))
                        .font(FlightTheme.label())
                        .foregroundStyle(FlightTheme.textTertiary)
                        .tracking(1)
                    if case .scheduled = resolved {
                        Circle()
                            .fill(FlightTheme.onTime)
                            .frame(width: 6, height: 6)
                    } else if case .fromFR24 = resolved {
                        Circle()
                            .fill(FlightTheme.onTime)
                            .frame(width: 6, height: 6)
                    }
                }
                resolvedRouteCard(resolved)
            }
        } else if isLoadingRoute {
            HStack {
                ProgressView()
                Text("Resolving route…")
                    .font(.caption)
                    .foregroundStyle(FlightTheme.textSecondary)
            }
        } else {
            VStack(alignment: .leading, spacing: 6) {
                Text("ROUTE")
                    .font(FlightTheme.label())
                    .foregroundStyle(FlightTheme.textTertiary)
                    .tracking(1)
                Text("Route unavailable. This aircraft isn't in any schedule source we cover (callsign \(aircraft.trimmedCallsign ?? aircraft.icao24)).")
                    .font(.caption)
                    .foregroundStyle(FlightTheme.textSecondary)
                    .padding(12)
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .background(FlightTheme.cardBackground, in: RoundedRectangle(cornerRadius: 12))
            }
        }
    }

    private func headerLabel(for r: ResolvedRoute) -> String {
        switch r {
        case .fromFR24:
            return aircraft.onGround ? "FLIGHT (ON GROUND)" : "IN FLIGHT"
        case .scheduled:
            return aircraft.onGround ? "FLIGHT (ON GROUND)" : "IN FLIGHT"
        case .fromOpenSky(_, let hoursAgo):
            if hoursAgo < 1 { return "LAST FLIGHT · JUST LANDED" }
            if hoursAgo < 24 { return "LAST FLIGHT · \(hoursAgo)h AGO" }
            return "LAST FLIGHT · \(hoursAgo / 24)d AGO"
        case .inferred:
            return aircraft.onGround ? "ON GROUND" : "IN FLIGHT (INFERRED)"
        }
    }

    @ViewBuilder
    private func resolvedRouteCard(_ r: ResolvedRoute) -> some View {
        switch r {
        case .fromFR24(let dep, let arr, _):
            VStack(alignment: .leading, spacing: 12) {
                HStack(spacing: 16) {
                    fr24Endpoint(
                        iata: dep,
                        label: aircraft.onGround ? "From" : "Departed",
                        emptyText: "Unknown"
                    )
                    Image(systemName: "airplane")
                        .font(.title3)
                        .foregroundStyle(FlightTheme.accent)
                        .rotationEffect(.degrees(-45))
                    fr24Endpoint(
                        iata: arr,
                        label: aircraft.onGround ? "To" : "Heading to",
                        emptyText: "Unknown"
                    )
                }
            }
            .flightCard()

        case .scheduled(let f):
            VStack(alignment: .leading, spacing: 12) {
                HStack(spacing: 16) {
                    routeEndpoint(
                        code: f.departure.airportIata,
                        label: aircraft.onGround ? "From" : "Departed",
                        time: f.departure.dateTime
                    )
                    Image(systemName: "airplane")
                        .font(.title3)
                        .foregroundStyle(FlightTheme.accent)
                        .rotationEffect(.degrees(-45))
                    routeEndpoint(
                        code: f.arrival.airportIata,
                        label: aircraft.onGround ? "To" : "Heading to",
                        time: f.arrival.dateTime
                    )
                }
            }
            .flightCard()

        case .fromOpenSky(let f, _):
            VStack(alignment: .leading, spacing: 12) {
                HStack(spacing: 16) {
                    routeEndpoint(
                        code: f.estDepartureAirport,
                        label: "Departed",
                        time: f.departureDate
                    )
                    Image(systemName: "airplane")
                        .font(.title3)
                        .foregroundStyle(FlightTheme.accent)
                        .rotationEffect(.degrees(-45))
                    routeEndpoint(
                        code: f.estArrivalAirport,
                        label: "Arrived",
                        time: f.arrivalDate
                    )
                }
            }
            .flightCard()

        case .inferred(let depIata, let depName):
            VStack(alignment: .leading, spacing: 12) {
                HStack(spacing: 16) {
                    VStack(alignment: .leading, spacing: 2) {
                        Text("Departed")
                            .font(.caption2)
                            .foregroundStyle(FlightTheme.textTertiary)
                            .tracking(0.5)
                        Text(depIata)
                            .font(FlightTheme.airportCode(28))
                            .foregroundStyle(FlightTheme.textPrimary)
                        if let depName {
                            Text(depName)
                                .font(.caption2)
                                .foregroundStyle(FlightTheme.textSecondary)
                                .lineLimit(1)
                        }
                    }
                    .frame(maxWidth: .infinity, alignment: .leading)
                    Image(systemName: "airplane")
                        .font(.title3)
                        .foregroundStyle(FlightTheme.accent)
                        .rotationEffect(.degrees(-45))
                    VStack(alignment: .leading, spacing: 2) {
                        Text("Heading to")
                            .font(.caption2)
                            .foregroundStyle(FlightTheme.textTertiary)
                            .tracking(0.5)
                        Text("—")
                            .font(FlightTheme.airportCode(28))
                            .foregroundStyle(FlightTheme.textTertiary)
                        Text("Not in schedule data")
                            .font(.caption2)
                            .foregroundStyle(FlightTheme.textSecondary)
                            .lineLimit(1)
                    }
                    .frame(maxWidth: .infinity, alignment: .leading)
                }
            }
            .flightCard()
        }
    }

    /// Endpoint cell for FR24-sourced routes. Different from
    /// `routeEndpoint` because FR24 gives us IATA codes directly (no
    /// ICAO-to-IATA conversion) and no scheduled time.
    private func fr24Endpoint(iata: String?, label: String, emptyText: String) -> some View {
        let code = iata ?? ""
        let cityName: String = {
            guard let iata, let m = database.airport(byIATA: iata) else { return "" }
            return m.name
        }()
        return VStack(alignment: .leading, spacing: 2) {
            Text(label)
                .font(.caption2)
                .foregroundStyle(FlightTheme.textTertiary)
                .tracking(0.5)
            Text(code.isEmpty ? "—" : code)
                .font(FlightTheme.airportCode(28))
                .foregroundStyle(code.isEmpty ? FlightTheme.textTertiary : FlightTheme.textPrimary)
            if !cityName.isEmpty {
                Text(cityName)
                    .font(.caption2)
                    .foregroundStyle(FlightTheme.textSecondary)
                    .lineLimit(1)
            } else if code.isEmpty {
                Text(emptyText)
                    .font(.caption2)
                    .foregroundStyle(FlightTheme.textSecondary)
                    .lineLimit(1)
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    private func routeEndpoint(code: String?, label: String, time: Date) -> some View {
        let iata = code.flatMap(icaoToIATA(_:)) ?? code ?? "—"
        let cityName: String = {
            guard let code, let m = database.airport(byIATA: icaoToIATA(code) ?? code) else { return "" }
            return m.name
        }()

        return VStack(alignment: .leading, spacing: 2) {
            Text(label)
                .font(.caption2)
                .foregroundStyle(FlightTheme.textTertiary)
                .tracking(0.5)
            Text(iata)
                .font(FlightTheme.airportCode(28))
                .foregroundStyle(FlightTheme.textPrimary)
            if !cityName.isEmpty {
                Text(cityName)
                    .font(.caption2)
                    .foregroundStyle(FlightTheme.textSecondary)
                    .lineLimit(1)
            }
            Text(shortDateTime(time))
                .font(.caption2.monospaced())
                .foregroundStyle(FlightTheme.textTertiary)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    private func recentFlightRow(_ f: OpenSkyFlight) -> some View {
        let dep = f.estDepartureAirport.flatMap(icaoToIATA(_:)) ?? f.estDepartureAirport ?? "—"
        let arr = f.estArrivalAirport.flatMap(icaoToIATA(_:)) ?? f.estArrivalAirport ?? "—"
        return HStack(spacing: 12) {
            Text(dep)
                .font(.subheadline.weight(.semibold).monospaced())
                .foregroundStyle(FlightTheme.textPrimary)
                .frame(width: 56, alignment: .leading)
            Image(systemName: "arrow.right")
                .font(.caption)
                .foregroundStyle(FlightTheme.textTertiary)
            Text(arr)
                .font(.subheadline.weight(.semibold).monospaced())
                .foregroundStyle(FlightTheme.textPrimary)
                .frame(width: 56, alignment: .leading)
            Spacer()
            Text(shortDate(f.departureDate))
                .font(.caption.monospaced())
                .foregroundStyle(FlightTheme.textSecondary)
        }
        .padding(.horizontal, 12)
        .padding(.vertical, 10)
        .background(FlightTheme.cardBackground, in: RoundedRectangle(cornerRadius: 10))
    }

    // MARK: - Aircraft card

    private var aircraftCard: some View {
        VStack(spacing: 0) {
            HStack(spacing: 0) {
                statCell(label: "ICAO24", value: aircraft.icao24.uppercased())
                statCell(label: "Country", value: aircraft.originCountry.isEmpty ? "—" : aircraft.originCountry)
            }
            if let cat = aircraft.category, let name = aircraftCategoryName(cat) {
                Divider()
                HStack(spacing: 0) {
                    statCell(label: "Category", value: name)
                    statCell(label: "Position", value: shortCoord(aircraft.coordinate))
                }
            } else {
                Divider()
                HStack(spacing: 0) {
                    statCell(label: "Position", value: shortCoord(aircraft.coordinate))
                    statCell(label: "", value: "")
                }
            }
        }
        .flightCard(padding: 0)
    }

    // MARK: - Helpers

    private var airlineEntry: AircraftRegistry.Entry? {
        AircraftRegistry.shared.lookup(icao: aircraft.airlineICAO)
    }

    private var airlineDisplayName: String {
        airlineEntry?.name
            ?? aircraft.airlineICAO?.uppercased()
            ?? "Unknown"
    }

    private func formatNumber(_ n: Int) -> String {
        let f = NumberFormatter()
        f.numberStyle = .decimal
        return f.string(from: NSNumber(value: n)) ?? "\(n)"
    }

    private func shortCoord(_ c: CLLocationCoordinate2D) -> String {
        String(format: "%.2f, %.2f", c.latitude, c.longitude)
    }

    private func shortTime(_ d: Date) -> String {
        let f = DateFormatter()
        f.timeStyle = .short
        return f.string(from: d)
    }

    private func shortDate(_ d: Date) -> String {
        let f = DateFormatter()
        f.dateFormat = "MMM d"
        return f.string(from: d)
    }

    private func shortDateTime(_ d: Date) -> String {
        let f = DateFormatter()
        f.dateFormat = "MMM d, HH:mm"
        return f.string(from: d)
    }

    /// OpenSky returns 4-letter ICAO airport codes (e.g. "KDFW", "EGLL").
    /// Resolve to IATA via the airport DB so non-US/CA/MX codes (LHR =
    /// "EGLL", FRA = "EDDF", etc.) round-trip correctly instead of being
    /// mangled into bogus 4-letter IATAs.
    ///
    /// 3-letter input is treated as already-IATA. 4-letter input is run
    /// through ``AirportDatabase.iata(forICAO:)`` which both applies the
    /// regional prefix-drop heuristic AND verifies the result against
    /// the bundled airport list. Returns nil when the ICAO can't be
    /// resolved — callers should hide rather than display a bad code.
    private func icaoToIATA(_ icao: String?) -> String? {
        guard let icao else { return nil }
        let s = icao.uppercased()
        if s.count == 3 { return s }      // Already IATA.
        guard s.count == 4 else { return nil }
        return database.iata(forICAO: s)
    }


    // MARK: - Enrichment context
    //
    // Everything below this line is Phase-2 enrichment scaffolding —
    // load-factor / OTP / weather / TSA / cascade / sister-flight cards.
    // The cards lazily render based on whatever signals we can extract
    // from the resolved route + the live aircraft.

    /// Distilled per-flight identifiers used as inputs to the enrichment
    /// services. Returns nil if we don't have at minimum a carrier IATA +
    /// flight number — without those nothing downstream is meaningful.
    private struct FlightContext {
        let carrierIATA: String
        let flightNumber: Int
        let originIATA: String?
        let destIATA: String?
        let scheduledDeparture: Date?
        let scheduledEquipmentIATA: String?
        let liveSeats: Int?
    }

    private var flightContext: FlightContext? {
        // Carrier + flight number — required.
        let carrierIATA: String? = {
            if let icao = aircraft.airlineICAO,
               let entry = AircraftRegistry.shared.lookup(icao: icao),
               let iata = entry.iata, !iata.isEmpty {
                return iata.uppercased()
            }
            return nil
        }()
        guard let carrier = carrierIATA,
              let fnStr = aircraft.flightNumber,
              let fn = Int(fnStr) else {
            return nil
        }

        // Origin / dest — pull from whichever resolved-route case has them.
        var origin: String?
        var dest: String?
        var scheduledDep: Date?
        var scheduledEquip: String?

        switch resolvedRoute {
        case .fromFR24(let dep, let arr, _):
            origin = dep
            dest = arr
        case .scheduled(let f):
            origin = f.departure.airportIata
            dest = f.arrival.airportIata
            scheduledDep = f.departure.dateTime
            scheduledEquip = f.equipmentIata
        case .fromOpenSky(let f, _):
            origin = f.estDepartureAirport.flatMap(icaoToIATA(_:))
            dest = f.estArrivalAirport.flatMap(icaoToIATA(_:))
        case .inferred(let dep, _):
            origin = dep
        case .none:
            break
        }

        return FlightContext(
            carrierIATA: carrier,
            flightNumber: fn,
            originIATA: origin?.uppercased(),
            destIATA: dest?.uppercased(),
            scheduledDeparture: scheduledDep,
            scheduledEquipmentIATA: scheduledEquip,
            liveSeats: nil
        )
    }

    // MARK: - Enrichment loader

    /// Fans out to each backend in sequence. Each service returns nil on
    /// missing data so the corresponding card simply doesn't render.
    /// `try Task.checkCancellation()` is threaded between sections so a
    /// re-firing `.task(id:)` (or a dismissed sheet) cleanly tears down
    /// the in-flight work instead of writing into stale @State.
    private func loadEnrichments() async {
        guard let ctx = flightContext else {
            // No usable context yet — typically because resolvedRoute is
            // still nil. The `.task(id:)` modifier will re-fire once the
            // id changes (i.e. once resolvedRoute populates).
            print("[LiveDetail] no flight context — skipping enrichments")
            return
        }

        let now = Date()
        let depDate = ctx.scheduledDeparture ?? now

        do {
            // Authoritative BTS bundle citation — used by the load-factor
            // and on-time cards so the period label is sourced from the
            // metadata file rather than any individual record's
            // samplePeriod.
            let meta = await BTSDataStore.shared.metadata()
            try Task.checkCancellation()
            await MainActor.run { btsMetadata = meta }

            // Load-factor estimate (BTS-backed). Needs origin + dest. We
            // pass the airport database so the service can resolve the
            // origin airport's timezone for accurate weekday + month
            // adjustments.
            if let origin = ctx.originIATA, let dest = ctx.destIATA {
                let estimate = await LoadFactorService.shared.estimate(
                    carrier: ctx.carrierIATA,
                    flightNumber: ctx.flightNumber,
                    origin: origin,
                    dest: dest,
                    date: depDate,
                    database: database,
                    liveSeats: ctx.liveSeats
                )
                try Task.checkCancellation()
                await MainActor.run { loadFactor = estimate }
            }

            // On-time historical stats. Needs origin + dest.
            if let origin = ctx.originIATA, let dest = ctx.destIATA {
                let stat = await OnTimePerformanceService.shared.stat(
                    carrier: ctx.carrierIATA,
                    flightNumber: ctx.flightNumber,
                    origin: origin,
                    dest: dest
                )
                try Task.checkCancellation()
                await MainActor.run { onTimeStat = stat }
            }

            // Equipment swap — needs scheduled IATA equipment plus live
            // ICAO type. We pass the carrier so the seat lookup prefers
            // the per-airline cabin layout over the generic default.
            //
            // FR24-sourced flights don't carry scheduled equipment, so
            // we also pass a BTS-derived baseline (route's typical seat
            // count) when available. That keeps the card useful for the
            // primary live path — "today's plane vs typical for this
            // route" instead of bailing entirely.
            if ctx.scheduledEquipmentIATA != nil || aircraft.typeCode != nil {
                var btsBaselineSeats: Int?
                if let origin = ctx.originIATA, let dest = ctx.destIATA {
                    if let rec = await BTSDataStore.shared.record(
                        carrier: ctx.carrierIATA,
                        flightNumber: ctx.flightNumber,
                        origin: origin,
                        dest: dest
                    ) {
                        btsBaselineSeats = rec.avgSeats
                    }
                }
                let swap = await EquipmentSwapService.shared.check(
                    scheduledEquipmentIATA: ctx.scheduledEquipmentIATA,
                    liveEquipmentICAO: aircraft.typeCode,
                    carrier: ctx.carrierIATA,
                    btsBaselineSeats: btsBaselineSeats
                )
                try Task.checkCancellation()
                await MainActor.run { equipmentSwap = swap }
            }

            // Weather at each endpoint we know about. WeatherClient.shared
            // caches internally so concurrent calls across views are cheap.
            if let origin = ctx.originIATA {
                let forecast = await WeatherClient.shared.forecast(forIATA: origin, on: depDate, database: database)
                try Task.checkCancellation()
                await MainActor.run { originWeather = forecast }
            }
            if let dest = ctx.destIATA {
                let forecast = await WeatherClient.shared.forecast(forIATA: dest, on: depDate, database: database)
                try Task.checkCancellation()
                await MainActor.run { arrivalWeather = forecast }
            }

            // Aircraft status — most recent rotation segment for the
            // operating aircraft. FR24-path fallback for the cascade
            // card: we don't have a scheduled departure to compare
            // against, but we CAN show the most recent landed leg
            // ("Just arrived from BWI 14:32") so the user has live
            // operational context. Refreshes in the 60s timer below
            // so the relative time doesn't go stale.
            if !aircraft.icao24.isEmpty {
                let segments = await AircraftRotationTracker.shared.rotation(
                    forICAO24: aircraft.icao24, lookbackHours: 6
                )
                try Task.checkCancellation()
                await MainActor.run { aircraftStatus = segments.last }
            }

            // Cascade risk. Skipped when the predictor can't see a clean
            // upstream segment.
            if let origin = ctx.originIATA, let dep = ctx.scheduledDeparture {
                // Hand the predictor the raw IATA. The predictor has its
                // own normaliser that compares IATA-vs-IATA and
                // ICAO-vs-ICAO, so hardcoding a "K" prefix here isn't
                // needed (and would be wrong for non-US/CA/MX airports
                // anyway). AircraftRotationTracker actually populates its
                // `arrivalICAO`/`departureICAO` from `MapAirport.iata`
                // today, so the bare IATA is the form that already
                // matches.
                let pred = await DelayCascadePredictor.shared.predict(
                    carrier: ctx.carrierIATA,
                    flightNumber: ctx.flightNumber,
                    scheduledDeparture: dep,
                    departureICAO: origin,
                    operatingICAO24: aircraft.icao24
                )
                try Task.checkCancellation()
                await MainActor.run { cascade = pred }
            }

            // Sister flights (alternate A→B options today).
            if let origin = ctx.originIATA, let dest = ctx.destIATA {
                let sisterSvc = SisterFlightService(
                    flightService: FlightService.shared,
                    loadPredictor: { [database] carrier, fn, date in
                        await LoadFactorService.shared.estimate(
                            carrier: carrier,
                            flightNumber: fn,
                            origin: origin,
                            dest: dest,
                            date: date,
                            database: database
                        )?.predicted
                    }
                )
                let result = await sisterSvc.sisterFlights(
                    origin: origin,
                    dest: dest,
                    date: depDate,
                    currentFlight: (carrier: ctx.carrierIATA, number: ctx.flightNumber)
                )
                try Task.checkCancellation()
                await MainActor.run { sisters = result }
            }
        } catch is CancellationError {
            // Sheet dismissed or route re-resolved — abandon work cleanly.
            print("[LiveDetail] enrichment cancelled")
        } catch {
            print("[LiveDetail] enrichment error: \(error)")
        }
    }

    /// Re-runs ONLY the cascade prediction + aircraft status so those
    /// cards can refresh on a timer without rolling the whole enrichment
    /// fan-out (network heavy). Both depend on aircraft rotation data
    /// that changes as the day progresses, so refreshing every 60s keeps
    /// the user from staring at a static snapshot.
    private func refreshCascadeOnly() async {
        // Cascade prediction needs scheduledDeparture; aircraft status
        // doesn't. Fan out both queries unconditionally so each card
        // surfaces fresh data independently.
        if let ctx = flightContext,
           let origin = ctx.originIATA,
           let dep = ctx.scheduledDeparture {
            let pred = await DelayCascadePredictor.shared.predict(
                carrier: ctx.carrierIATA,
                flightNumber: ctx.flightNumber,
                scheduledDeparture: dep,
                departureICAO: origin,
                operatingICAO24: aircraft.icao24
            )
            if Task.isCancelled { return }
            await MainActor.run { cascade = pred }
        }

        if !aircraft.icao24.isEmpty {
            let segments = await AircraftRotationTracker.shared.rotation(
                forICAO24: aircraft.icao24, lookbackHours: 6
            )
            if Task.isCancelled { return }
            await MainActor.run { aircraftStatus = segments.last }
        }
    }

    // MARK: - Enrichment sections

    @ViewBuilder
    private var enrichmentSections: some View {
        VStack(alignment: .leading, spacing: 16) {
            loadFactorCard
            onTimeCard
            equipmentCard
            weatherCards
            // Cascade renders when we can predict a propagated delay
            // (needs scheduled departure). Otherwise the aircraft-status
            // card surfaces — same rotation tracker, different framing.
            // Only one of the two should render at a time.
            if cascade != nil {
                cascadeCard
            } else {
                aircraftStatusCard
            }
            sisterFlightsCard
        }
        .padding(.top, 4)
    }

    // MARK: Aircraft status card (FR24 path fallback for cascade)

    @ViewBuilder
    private var aircraftStatusCard: some View {
        if let segment = aircraftStatus {
            VStack(alignment: .leading, spacing: 8) {
                Text("AIRCRAFT STATUS")
                    .font(FlightTheme.label())
                    .foregroundStyle(FlightTheme.textTertiary)
                    .tracking(1)

                VStack(alignment: .leading, spacing: 8) {
                    HStack(alignment: .firstTextBaseline) {
                        Text(statusHeadline(for: segment))
                            .font(.subheadline.weight(.bold))
                            .foregroundStyle(FlightTheme.textPrimary)
                        Spacer()
                        Text(shortTime(segment.arrivalTime))
                            .font(.caption.weight(.semibold).monospaced())
                            .foregroundStyle(FlightTheme.textSecondary)
                    }
                    if let basis = statusDetail(for: segment) {
                        Text(basis)
                            .font(.caption2)
                            .foregroundStyle(FlightTheme.textSecondary)
                            .fixedSize(horizontal: false, vertical: true)
                    }
                }
                .flightCard()
            }
        }
    }

    /// Short headline for the aircraft-status card. If the aircraft has
    /// already landed (arrival time < now), the card shows the inbound
    /// origin. If it's still airborne we frame it as an ETA.
    private func statusHeadline(for segment: AircraftRotationTracker.RotationSegment) -> String {
        let from = segment.departureICAO ?? "?"
        if segment.arrivalTime <= Date() {
            return "Just arrived from \(from)"
        }
        return "Inbound from \(from)"
    }

    private func statusDetail(for segment: AircraftRotationTracker.RotationSegment) -> String? {
        let depart = shortTime(segment.departureTime)
        let arrive = shortTime(segment.arrivalTime)
        let from = segment.departureICAO ?? "?"
        let to = segment.arrivalICAO ?? "?"
        // OpenSky's /flights/aircraft only returns LANDED flights — so this
        // is always "last completed leg", not "current position". Make
        // that explicit so the user doesn't read it as live tracking.
        let stalenessHours = Int(Date().timeIntervalSince(segment.arrivalTime) / 3600)
        let staleness = stalenessHours <= 0
            ? "just now"
            : (stalenessHours == 1 ? "1 hour ago" : "\(stalenessHours) hours ago")
        return "Last landed leg: \(from) \(depart) → \(to) \(arrive) (\(staleness)). No scheduled departure available — cascade prediction skipped."
    }

    // MARK: Load-factor card

    @ViewBuilder
    private var loadFactorCard: some View {
        if let lf = loadFactor {
            VStack(alignment: .leading, spacing: 8) {
                Text("PREDICTED LOAD")
                    .font(FlightTheme.label())
                    .foregroundStyle(FlightTheme.textTertiary)
                    .tracking(1)

                VStack(alignment: .leading, spacing: 10) {
                    HStack(alignment: .firstTextBaseline) {
                        Text("\(Int(round(lf.predicted * 100)))%")
                            .font(.title2.weight(.bold).monospaced())
                            .foregroundStyle(loadColor(for: lf.predicted))
                        Spacer()
                        Text("conf \(Int(round(lf.confidence * 100)))%")
                            .font(.caption2.monospaced())
                            .foregroundStyle(FlightTheme.textTertiary)
                    }
                    loadBar(value: lf.predicted)
                    Text(lf.basis)
                        .font(.caption2)
                        .foregroundStyle(FlightTheme.textSecondary)
                        .fixedSize(horizontal: false, vertical: true)
                    // Authoritative bundle citation. Sourced from the
                    // companion meta file rather than any per-record field
                    // so the period can't drift between cards.
                    if let meta = btsMetadata {
                        Text("Source: DOT BTS \(meta.sourcePeriod) · \(meta.recordCount) records")
                            .font(.caption2)
                            .foregroundStyle(FlightTheme.textTertiary)
                    }
                }
                .flightCard()
            }
        }
    }

    private func loadColor(for value: Double) -> Color {
        if value > 0.85 { return FlightTheme.cancelled }
        if value > 0.70 { return FlightTheme.delayed }
        return FlightTheme.onTime
    }

    private func loadBar(value: Double) -> some View {
        GeometryReader { geo in
            ZStack(alignment: .leading) {
                Capsule()
                    .fill(FlightTheme.elevatedBackground)
                Capsule()
                    .fill(loadColor(for: value))
                    .frame(width: max(0, geo.size.width * min(1.0, max(0.0, value))))
            }
        }
        .frame(height: 8)
    }

    // MARK: On-time history card

    @ViewBuilder
    private var onTimeCard: some View {
        if let stat = onTimeStat {
            VStack(alignment: .leading, spacing: 8) {
                Text("ON-TIME HISTORY")
                    .font(FlightTheme.label())
                    .foregroundStyle(FlightTheme.textTertiary)
                    .tracking(1)

                VStack(spacing: 0) {
                    HStack(spacing: 0) {
                        statCell(label: "On-time",
                                 value: "\(Int(round(stat.onTimePct * 100)))%")
                        statCell(label: "Avg delay",
                                 value: String(format: "%.0f min", stat.avgDelayMin))
                    }
                    Divider()
                    HStack(spacing: 0) {
                        statCell(label: "Cancelled",
                                 value: String(format: "%.1f%%", stat.cancelledPct * 100))
                        statCell(label: "Sample",
                                 value: "\(stat.n) flts")
                    }
                }
                .flightCard(padding: 0)

                // Prefer the authoritative bundle metadata period over the
                // per-record samplePeriod (they should match, but the meta
                // file is the citation source-of-truth).
                Text(btsPeriodLabel(fallback: stat.samplePeriod))
                    .font(.caption2)
                    .foregroundStyle(FlightTheme.textTertiary)
            }
        }
    }

    /// Format the BTS sample period for in-card footers. Pulls from
    /// ``btsMetadata`` when loaded, falling back to whatever the per-record
    /// or per-estimate value provided.
    private func btsPeriodLabel(fallback: String) -> String {
        let period = btsMetadata?.sourcePeriod ?? fallback
        return "BTS \(period)"
    }

    // MARK: Equipment swap card

    @ViewBuilder
    private var equipmentCard: some View {
        if let swap = equipmentSwap {
            VStack(alignment: .leading, spacing: 8) {
                Text("EQUIPMENT TODAY")
                    .font(FlightTheme.label())
                    .foregroundStyle(FlightTheme.textTertiary)
                    .tracking(1)

                VStack(alignment: .leading, spacing: 10) {
                    HStack(spacing: 12) {
                        equipmentColumn(
                            title: "Scheduled",
                            name: swap.scheduledName,
                            seats: swap.scheduledSeats
                        )
                        Image(systemName: "arrow.right")
                            .font(.caption)
                            .foregroundStyle(FlightTheme.textTertiary)
                        equipmentColumn(
                            title: "Operating",
                            name: swap.liveName ?? "Unknown",
                            seats: swap.liveSeats
                        )
                    }
                    if swap.severity != .none, let delta = swap.seatDelta {
                        equipmentSwapBadge(delta: delta, severity: swap.severity)
                    }
                    Text(swap.summary)
                        .font(.caption2)
                        .foregroundStyle(FlightTheme.textSecondary)
                        .fixedSize(horizontal: false, vertical: true)
                }
                .flightCard()
            }
        }
    }

    private func equipmentColumn(title: String, name: String, seats: Int?) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(title)
                .font(.caption2)
                .foregroundStyle(FlightTheme.textTertiary)
                .tracking(0.5)
            Text(name)
                .font(.subheadline.weight(.semibold))
                .foregroundStyle(FlightTheme.textPrimary)
                .lineLimit(1)
            if let seats {
                Text("\(seats) seats")
                    .font(.caption2.monospaced())
                    .foregroundStyle(FlightTheme.textSecondary)
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    private func equipmentSwapBadge(delta: Int, severity: EquipmentSwapService.SwapSeverity) -> some View {
        let color: Color = (severity == .significant) ? FlightTheme.delayed : FlightTheme.accent
        let prefix = delta > 0 ? "+" : ""
        return Text("\(prefix)\(delta) seats")
            .font(.caption.weight(.bold))
            .foregroundStyle(.white)
            .padding(.horizontal, 10)
            .padding(.vertical, 4)
            .background(color, in: Capsule())
    }

    // MARK: Weather cards

    @ViewBuilder
    private var weatherCards: some View {
        if originWeather != nil || arrivalWeather != nil {
            VStack(alignment: .leading, spacing: 8) {
                Text("WEATHER")
                    .font(FlightTheme.label())
                    .foregroundStyle(FlightTheme.textTertiary)
                    .tracking(1)

                HStack(spacing: 12) {
                    if let w = originWeather {
                        weatherCard(label: "Origin", forecast: w)
                    }
                    if let w = arrivalWeather {
                        weatherCard(label: "Arrival", forecast: w)
                    }
                }
            }
        }
    }

    private func weatherCard(label: String, forecast: WeatherForecast) -> some View {
        VStack(alignment: .leading, spacing: 6) {
            HStack(spacing: 6) {
                Text(label)
                    .font(.caption2)
                    .foregroundStyle(FlightTheme.textTertiary)
                    .tracking(0.5)
                Text(forecast.airport)
                    .font(.caption2.weight(.semibold).monospaced())
                    .foregroundStyle(FlightTheme.textSecondary)
                Spacer()
                weatherRiskDot(forecast.riskScore)
            }
            Text(forecast.summary)
                .font(.subheadline.weight(.semibold))
                .foregroundStyle(FlightTheme.textPrimary)
                .lineLimit(1)
            HStack(spacing: 10) {
                weatherMetric(systemImage: "thermometer.medium",
                              text: String(format: "%.0f°C", forecast.temperatureC))
                weatherMetric(systemImage: "wind",
                              text: String(format: "%.0f km/h", forecast.windKmh))
                weatherMetric(systemImage: "cloud.rain",
                              text: "\(forecast.precipProbabilityPct)%")
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .flightCard()
    }

    private func weatherMetric(systemImage: String, text: String) -> some View {
        HStack(spacing: 3) {
            Image(systemName: systemImage)
                .font(.caption2)
            Text(text)
                .font(.caption2.monospaced())
        }
        .foregroundStyle(FlightTheme.textSecondary)
    }

    private func weatherRiskDot(_ risk: WeatherRisk) -> some View {
        let color: Color
        switch risk {
        case .low:      color = FlightTheme.onTime
        case .moderate: color = FlightTheme.delayed
        case .high:     color = FlightTheme.cancelled
        }
        return Circle().fill(color).frame(width: 8, height: 8)
    }

    // MARK: Cascade risk card

    @ViewBuilder
    private var cascadeCard: some View {
        if let pred = cascade {
            VStack(alignment: .leading, spacing: 8) {
                Text("CASCADE RISK")
                    .font(FlightTheme.label())
                    .foregroundStyle(FlightTheme.textTertiary)
                    .tracking(1)

                VStack(alignment: .leading, spacing: 8) {
                    HStack(alignment: .firstTextBaseline) {
                        Text("+\(pred.predictedDelayMin) min")
                            .font(.title3.weight(.bold).monospaced())
                            .foregroundStyle(FlightTheme.delayed)
                        Spacer()
                        Text("conf \(Int(round(pred.confidence * 100)))%")
                            .font(.caption2.monospaced())
                            .foregroundStyle(FlightTheme.textTertiary)
                    }
                    Text(pred.basis)
                        .font(.caption2)
                        .foregroundStyle(FlightTheme.textSecondary)
                        .fixedSize(horizontal: false, vertical: true)
                }
                .flightCard()
            }
        }
    }

    // MARK: Sister flights card

    @ViewBuilder
    private var sisterFlightsCard: some View {
        let alternates = sisters.filter { !$0.isYourFlight }.prefix(5)
        if !alternates.isEmpty {
            VStack(alignment: .leading, spacing: 8) {
                Text("OTHER OPTIONS TODAY")
                    .font(FlightTheme.label())
                    .foregroundStyle(FlightTheme.textTertiary)
                    .tracking(1)

                VStack(spacing: 0) {
                    ForEach(Array(alternates.enumerated()), id: \.element.id) { idx, flight in
                        sisterRow(flight)
                        if idx < alternates.count - 1 {
                            Divider().padding(.leading, 16)
                        }
                    }
                }
                .flightCard(padding: 0)
            }
        }
    }

    private func sisterRow(_ flight: SisterFlightService.SisterFlight) -> some View {
        HStack(spacing: 12) {
            VStack(alignment: .leading, spacing: 2) {
                Text("\(flight.carrier)\(flight.flightNumber)")
                    .font(.subheadline.weight(.semibold).monospaced())
                    .foregroundStyle(FlightTheme.textPrimary)
                Text(shortTime(flight.scheduledDeparture))
                    .font(.caption2.monospaced())
                    .foregroundStyle(FlightTheme.textSecondary)
            }
            if let aircraft = flight.aircraftDisplay {
                Text(aircraft)
                    .font(.caption2)
                    .foregroundStyle(FlightTheme.textTertiary)
                    .lineLimit(1)
            }
            Spacer()
            if let load = flight.predictedLoad {
                Text("\(Int(round(load * 100)))%")
                    .font(.caption.weight(.bold).monospaced())
                    .foregroundStyle(loadColor(for: load))
            } else {
                Text("—")
                    .font(.caption.monospaced())
                    .foregroundStyle(FlightTheme.textTertiary)
            }
        }
        .padding(.horizontal, 16)
        .padding(.vertical, 10)
    }
}
