import SwiftUI
import MapKit
import CoreLocation

/// Single-flight detail screen — restyled with the passport palette.
/// Aircraft card now uses Flighty's labeled-grid pattern with
/// em-dashes for missing data. New "Detailed Timetable" card shows
/// scheduled vs actual when we have actual times, with late actuals
/// in red.
struct HistoryDetailView: View {
    let flight: LoggedFlight
    let store: FlightHistoryStore
    let database: AirportDatabase
    let openSky: OpenSkyClient
    var routeExplorer: RouteExplorerClient? = nil

    @Environment(\.dismiss) private var dismiss
    @Environment(\.openURL) private var openURL
    @Environment(\.colorScheme) private var scheme

    @State private var photo: AircraftPhotoService.Photo?
    @State private var track: AircraftTrack?
    @State private var editedNotes: String = ""
    @State private var showDeleteConfirm = false
    @State private var metadataLoaded = false

    // Standby editor state. Mirrors the persisted fields on LoggedFlight
    // so the Picker / DatePickers have stable bindings; onChange writes
    // back into the @Model and saves the context.
    @State private var standbyOutcome: String = "confirmed"
    @State private var standbyAttemptedAt: Date = Date()
    @State private var standbyClearedAt: Date = Date()
    @State private var standbyNotes: String = ""
    @State private var hasStandbyAttemptedAt: Bool = false
    @State private var hasStandbyClearedAt: Bool = false

    // Airframe history snapshot for the section below.
    @State private var airframeStats: AirframeHistoryStore.AirframeStats?

    private let airframeHistory = AirframeHistoryStore()

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 14) {
                header
                routeCard
                photoBanner.padding(.horizontal, -16)
                if let cred = photo?.photographer {
                    photoCredit(name: cred, link: photo?.detailLink)
                }
                mapSection
                aircraftCard
                timetableCard
                notesSection
                standbySection
                airframeHistorySection
                deleteButton
            }
            .padding(16)
        }
        .background(HistoryStyle.background(scheme).ignoresSafeArea())
        .navigationTitle("")
        .navigationBarTitleDisplayMode(.inline)
        .task {
            editedNotes = flight.notes ?? ""
            hydrateStandbyState()
            loadAirframeHistory()
            if let reg = flight.registration {
                photo = await AircraftPhotoService.shared.photo(registration: reg, icao24: flight.icao24 ?? "")
            }
            await loadTrackIfRecent()
            await loadAirframeMetadata()
            await enrichAircraftTypeIfMissing()
        }
        .alert("Delete this flight?", isPresented: $showDeleteConfirm) {
            Button("Delete", role: .destructive) {
                store.delete(flight)
                dismiss()
            }
            Button("Cancel", role: .cancel) {}
        }
    }

    // MARK: - Header

    private var header: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(flight.flightLabel)
                .font(.system(size: 38, weight: .black).monospaced())
                .foregroundStyle(HistoryStyle.ink(scheme))
            HStack(spacing: 8) {
                Text(airlineName)
                    .font(.system(size: 14, weight: .semibold))
                Text("·")
                Text(longDate(flight.flightDate).uppercased())
                    .font(.system(size: 12, weight: .heavy).monospaced())
                    .tracking(1)
            }
            .foregroundStyle(HistoryStyle.inkSecondary(scheme))
        }
    }

    private var airlineName: String {
        AircraftRegistry.shared.lookup(icao: flight.carrierICAO)?.name
            ?? AircraftRegistry.shared.lookup(iata: flight.carrierIATA)?.name
            ?? flight.carrierIATA ?? "Unknown"
    }

    /// Compass bearing from departure to arrival airport. Falls back to
    /// 90° (eastbound) when we can't resolve coordinates so the icon
    /// just stays in a sensible default.
    private var routeBearing: Double {
        guard let dep = database.airport(byIATA: flight.departureIATA),
              let arr = database.airport(byIATA: flight.arrivalIATA)
        else { return 90 }
        return Self.bearing(from: dep.coordinate, to: arr.coordinate)
    }

    static func bearing(from a: CLLocationCoordinate2D, to b: CLLocationCoordinate2D) -> Double {
        let lat1 = a.latitude * .pi / 180
        let lat2 = b.latitude * .pi / 180
        let dLon = (b.longitude - a.longitude) * .pi / 180
        let y = sin(dLon) * cos(lat2)
        let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon)
        let theta = atan2(y, x)
        return (theta * 180 / .pi + 360).truncatingRemainder(dividingBy: 360)
    }

    // MARK: - Route

    private var routeCard: some View {
        VStack(spacing: 14) {
            HStack(alignment: .top) {
                routeEndpoint(iata: flight.departureIATA, label: "From", time: flight.actualDeparture ?? flight.scheduledDeparture)
                Spacer()
                Image(systemName: "airplane")
                    .font(.system(size: 24, weight: .heavy))
                    .foregroundStyle(HistoryStyle.runwayOrange)
                    // SF airplane symbol naturally points up-right
                    // (~45°). To align with the actual travel
                    // bearing, rotate by (bearing - 45).
                    .rotationEffect(.degrees(routeBearing - 45))
                Spacer()
                routeEndpoint(iata: flight.arrivalIATA, label: "To", time: flight.actualArrival ?? flight.scheduledArrival)
            }
            Rectangle()
                .fill(HistoryStyle.hairline(scheme))
                .frame(height: 0.5)
            HStack(spacing: 18) {
                if let mi = store.distanceMiles(for: flight) {
                    miniStat(label: "Distance", value: "\(numberString(mi)) mi")
                }
                miniStat(label: "Duration", value: durationDisplay)
                Spacer()
            }
        }
        .historyCard(scheme, padding: 18)
    }

    private func routeEndpoint(iata: String, label: String, time: Date?) -> some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(label.uppercased())
                .font(HistoryStyle.label(10))
                .tracking(1.3)
                .foregroundStyle(HistoryStyle.inkTertiary(scheme))
            Text(iata.isEmpty ? "—" : iata)
                .font(.system(size: 32, weight: .black).monospaced())
                .foregroundStyle(HistoryStyle.ink(scheme))
            if let m = database.airport(byIATA: iata) {
                Text(m.name)
                    .font(.system(size: 11))
                    .foregroundStyle(HistoryStyle.inkSecondary(scheme))
                    .lineLimit(1)
            }
            if let time {
                Text(shortDateTime(time))
                    .font(.system(size: 11, weight: .semibold).monospaced())
                    .foregroundStyle(HistoryStyle.inkTertiary(scheme))
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    private func miniStat(label: String, value: String) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(label.uppercased())
                .font(HistoryStyle.label(9))
                .tracking(1.2)
                .foregroundStyle(HistoryStyle.inkTertiary(scheme))
            Text(value)
                .font(.system(size: 14, weight: .heavy).monospaced())
                .foregroundStyle(HistoryStyle.ink(scheme))
        }
    }

    // MARK: - Photo

    @ViewBuilder
    private var photoBanner: some View {
        if let photo {
            AsyncImage(url: photo.largeURL) { phase in
                switch phase {
                case .success(let img): img.resizable().aspectRatio(contentMode: .fill)
                default: Rectangle().fill(HistoryStyle.cardSubtle(scheme))
                }
            }
            .frame(maxWidth: .infinity)
            .frame(height: 200)
            .clipped()
        }
    }

    private func photoCredit(name: String, link: URL?) -> some View {
        HStack(spacing: 4) {
            Image(systemName: "camera.fill").font(.system(size: 9))
            Text("Photo by \(name) · planespotters.net")
                .font(.system(size: 10))
        }
        .foregroundStyle(HistoryStyle.inkTertiary(scheme))
        .contentShape(Rectangle())
        .onTapGesture { if let link { openURL(link) } }
    }

    // MARK: - Map

    @ViewBuilder
    private var mapSection: some View {
        VStack(alignment: .leading, spacing: 8) {
            HistorySectionLabel(track == nil ? "Route" : "Flown path")
            FlightRouteMap(
                departureIATA: flight.departureIATA,
                arrivalIATA: flight.arrivalIATA,
                track: track,
                database: database
            )
            .frame(height: 220)
            .clipShape(RoundedRectangle(cornerRadius: 18))
        }
    }

    private func loadTrackIfRecent() async {
        let ageDays = Date().timeIntervalSince(flight.flightDate) / 86400
        guard ageDays < 7, let icao24 = flight.icao24, !icao24.isEmpty else { return }
        track = await openSky.track(icao24: icao24)
    }

    /// If the flight is missing its aircraft type, run the same
    /// two-step lookup the bulk enricher does: route-explorer for
    /// future flights, FlightAware activity-log scrape for historical.
    /// Result is normalized to ICAO and patched onto the LoggedFlight.
    /// No-op when type is already set.
    private func enrichAircraftTypeIfMissing() async {
        guard flight.aircraftType == nil || flight.aircraftType?.isEmpty == true else { return }
        guard let carrier = flight.carrierIATA,
              let numStr = flight.flightNumber,
              let num = Int(numStr)
        else { return }

        // 1) route-explorer (works for future schedules)
        if let routeExplorer {
            let day = Calendar.current.startOfDay(for: flight.flightDate)
            let next = Calendar.current.date(byAdding: .day, value: 1, to: day) ?? day
            let results = await routeExplorer.searchSchedule(
                carrierCode: carrier,
                flightNumber: num,
                startDate: day,
                endDate: next
            )
            let exact = results.first {
                $0.departure.airportIata == flight.departureIATA
                    && $0.arrival.airportIata == flight.arrivalIATA
            } ?? results.first
            if let eq = exact?.equipmentIata, !eq.isEmpty {
                flight.aircraftType = AircraftDatabase.shared.normalizedICAO(forCode: eq)
                store.persist("update flight")
                return
            }
        }

        // 2) FlightAware activity-log fallback
        guard let carrierICAO = flight.carrierICAO
            ?? AircraftRegistry.shared.lookup(iata: carrier)?.icao
        else { return }
        let callsign = "\(carrierICAO)\(num)"
        if let icaoType = await FlightAwareLookup.shared.lookupType(
            callsign: callsign,
            departureIATA: flight.departureIATA,
            arrivalIATA: flight.arrivalIATA
        ) {
            flight.aircraftType = AircraftDatabase.shared.normalizedICAO(forCode: icaoType)
            store.persist("update flight")
        }
    }

    private func loadAirframeMetadata() async {
        guard let reg = flight.registration, !reg.isEmpty,
              let icao24 = flight.icao24, !icao24.isEmpty
        else { return }
        if let cached = store.airframe(for: reg),
           cached.firstFlightDate != nil || cached.deliveryDate != nil {
            metadataLoaded.toggle()
            return
        }
        if let meta = await AirframeMetadataService.shared.metadata(forICAO24: icao24) {
            store.upsertAirframe(
                registration: reg,
                firstFlightDate: meta.firstFlightDate,
                deliveryDate: meta.built
            )
            metadataLoaded.toggle()
        }
    }

    // MARK: - Aircraft card

    private var aircraftCard: some View {
        let repeats = store.repeatCount(for: flight.registration, before: flight.flightDate)
        let airframe = flight.registration.flatMap(store.airframe(for:))
        let firstFlight = airframe?.firstFlightDate
        let ageYears = firstFlight.map { years(since: $0) }
        let typeDisplay: String = {
            guard let code = flight.aircraftType, !code.isEmpty else { return "—" }
            let friendly = AircraftDatabase.shared.displayName(forTypeCode: code)
            return friendly == code ? code : "\(code) · \(friendly)"
        }()

        return VStack(alignment: .leading, spacing: 12) {
            HistorySectionLabel("Aircraft")
            VStack(spacing: 0) {
                aircraftRow(
                    leftLabel: "Type", leftValue: typeDisplay,
                    rightLabel: "Tail #", rightValue: flight.registration ?? "—"
                )
                divider
                aircraftRow(
                    leftLabel: "First flight",
                    leftValue: firstFlight.map { yearString($0) } ?? "—",
                    rightLabel: "Age",
                    rightValue: ageYears.map { "\($0) yr" } ?? "—"
                )
                divider
                aircraftRow(
                    leftLabel: "On this airframe",
                    leftValue: repeats == 0 ? "First time" : "\(repeats + 1)\(ordinalSuffix(repeats + 1)) time",
                    rightLabel: "ICAO24",
                    rightValue: flight.icao24?.uppercased() ?? "—"
                )
            }
            .historyCard(scheme, padding: 0)

            // Honest note: tail / icao24 / first-flight come from
            // OpenSky metadata which requires icao24 — only captured
            // when the flight is added via the Live tab. Historical
            // CSV imports won't have these.
            if flight.registration == nil && flight.icao24 == nil {
                Text("Tail #, first-flight date and ICAO24 aren't available for this flight — those come from the live ADS-B feed when you tap a plane on the Live tab. Aircraft type was looked up from FlightAware's recent activity for this flight number.")
                    .font(.caption2)
                    .foregroundStyle(HistoryStyle.inkTertiary(scheme))
                    .padding(.top, 4)
            }
        }
    }

    private var divider: some View {
        Rectangle()
            .fill(HistoryStyle.hairline(scheme))
            .frame(height: 0.5)
    }

    private func aircraftRow(leftLabel: String, leftValue: String, rightLabel: String, rightValue: String) -> some View {
        HStack(spacing: 0) {
            cell(label: leftLabel, value: leftValue)
            cell(label: rightLabel, value: rightValue)
        }
    }

    private func cell(label: String, value: String) -> some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(label.uppercased())
                .font(HistoryStyle.label(10))
                .tracking(1.3)
                .foregroundStyle(HistoryStyle.inkTertiary(scheme))
            Text(value)
                .font(.system(size: 14, weight: .heavy).monospaced())
                .foregroundStyle(HistoryStyle.ink(scheme))
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(14)
    }

    // MARK: - Timetable

    @ViewBuilder
    private var timetableCard: some View {
        if hasTimetableData {
            VStack(alignment: .leading, spacing: 12) {
                HistorySectionLabel("Detailed timetable")
                VStack(spacing: 0) {
                    timetableHeader
                    divider
                    timetableRow(
                        label: "Departure",
                        scheduled: flight.scheduledDeparture,
                        actual: flight.actualDeparture
                    )
                    divider
                    timetableRow(
                        label: "Arrival",
                        scheduled: flight.scheduledArrival,
                        actual: flight.actualArrival
                    )
                }
                .historyCard(scheme, padding: 0)
            }
        }
    }

    private var hasTimetableData: Bool {
        flight.scheduledDeparture != nil
            || flight.scheduledArrival != nil
            || flight.actualDeparture != nil
            || flight.actualArrival != nil
    }

    private var timetableHeader: some View {
        HStack(spacing: 0) {
            Text("")
                .frame(maxWidth: .infinity, alignment: .leading)
            Text("SCHEDULED")
                .font(HistoryStyle.label(10))
                .tracking(1.2)
                .foregroundStyle(HistoryStyle.inkTertiary(scheme))
                .frame(maxWidth: .infinity, alignment: .leading)
            Text("ACTUAL")
                .font(HistoryStyle.label(10))
                .tracking(1.2)
                .foregroundStyle(HistoryStyle.inkTertiary(scheme))
                .frame(maxWidth: .infinity, alignment: .leading)
        }
        .padding(.horizontal, 14)
        .padding(.top, 14)
    }

    private func timetableRow(label: String, scheduled: Date?, actual: Date?) -> some View {
        let isLate: Bool = {
            guard let scheduled, let actual else { return false }
            return actual.timeIntervalSince(scheduled) > 5 * 60
        }()
        return HStack(spacing: 0) {
            Text(label)
                .font(.system(size: 13, weight: .semibold))
                .foregroundStyle(HistoryStyle.ink(scheme))
                .frame(maxWidth: .infinity, alignment: .leading)
            Text(scheduled.map(shortTime) ?? "—")
                .font(.system(size: 13, weight: .heavy).monospaced())
                .foregroundStyle(HistoryStyle.inkSecondary(scheme))
                .frame(maxWidth: .infinity, alignment: .leading)
            Text(actual.map(shortTime) ?? "—")
                .font(.system(size: 13, weight: .heavy).monospaced())
                .foregroundStyle(isLate ? Color(red: 0.85, green: 0.15, blue: 0.15) : HistoryStyle.ink(scheme))
                .frame(maxWidth: .infinity, alignment: .leading)
        }
        .padding(14)
    }

    // MARK: - Notes

    private var notesSection: some View {
        VStack(alignment: .leading, spacing: 8) {
            HistorySectionLabel("Notes")
            TextEditor(text: $editedNotes)
                .scrollContentBackground(.hidden)
                .frame(minHeight: 80)
                .padding(8)
                .background(HistoryStyle.card(scheme), in: RoundedRectangle(cornerRadius: 14))
                .onChange(of: editedNotes) { _, newValue in
                    flight.notes = newValue.isEmpty ? nil : newValue
                    // Bug: this used to be a silent assignment with no
                    // save call — every notes edit was wiped on dismiss.
                    store.persist("update notes")
                }
        }
    }

    // MARK: - Standby outcome

    /// Editable card for logging a nonrev / standby outcome. The Picker
    /// is always visible; the date pickers and the cleared-at date only
    /// appear for the standby outcomes (and only standby-made for
    /// cleared-at, since a bumped flight never cleared).
    private var standbySection: some View {
        VStack(alignment: .leading, spacing: 8) {
            HistorySectionLabel("Standby outcome")
            VStack(alignment: .leading, spacing: 14) {
                Picker("Outcome", selection: $standbyOutcome) {
                    Text("Confirmed").tag("confirmed")
                    Text("Standby — Made").tag("standby-made")
                    Text("Standby — Bumped").tag("standby-bumped")
                }
                .pickerStyle(.segmented)
                .tint(FlightTheme.accent)
                .onChange(of: standbyOutcome) { _, newValue in
                    flight.standbyOutcome = newValue
                    if newValue == "confirmed" {
                        // Confirmed flights don't carry standby
                        // timestamps; clear them so we never leak stale
                        // values after a user toggles back.
                        flight.standbyAttemptedAt = nil
                        flight.standbyClearedAt = nil
                        hasStandbyAttemptedAt = false
                        hasStandbyClearedAt = false
                    } else if newValue == "standby-bumped" {
                        // Bumped means the user never cleared the list.
                        // Persist the attempted-at date if the user hasn't
                        // touched the picker — otherwise the @State default
                        // (flight's scheduled departure) silently disappears
                        // when they leave the screen.
                        if !hasStandbyAttemptedAt {
                            flight.standbyAttemptedAt = standbyAttemptedAt
                            hasStandbyAttemptedAt = true
                        }
                        flight.standbyClearedAt = nil
                        hasStandbyClearedAt = false
                    } else if newValue == "standby-made" {
                        // Same lossless-default treatment as bumped, but
                        // also write the cleared-at default so a
                        // toggle-and-leave doesn't drop the timestamp.
                        if !hasStandbyAttemptedAt {
                            flight.standbyAttemptedAt = standbyAttemptedAt
                            hasStandbyAttemptedAt = true
                        }
                        if !hasStandbyClearedAt {
                            flight.standbyClearedAt = standbyClearedAt
                            hasStandbyClearedAt = true
                        }
                    }
                    store.persist("update standby outcome")
                }

                if standbyOutcome.contains("standby") {
                    VStack(alignment: .leading, spacing: 10) {
                        DatePicker(
                            "Attempted at",
                            selection: $standbyAttemptedAt,
                            displayedComponents: [.date, .hourAndMinute]
                        )
                        .font(.system(size: 13, weight: .semibold))
                        .foregroundStyle(HistoryStyle.ink(scheme))
                        .onChange(of: standbyAttemptedAt) { _, newValue in
                            flight.standbyAttemptedAt = newValue
                            hasStandbyAttemptedAt = true
                            store.persist("update standby outcome")
                        }

                        if standbyOutcome == "standby-made" {
                            DatePicker(
                                "Cleared at",
                                selection: $standbyClearedAt,
                                displayedComponents: [.date, .hourAndMinute]
                            )
                            .font(.system(size: 13, weight: .semibold))
                            .foregroundStyle(HistoryStyle.ink(scheme))
                            .onChange(of: standbyClearedAt) { _, newValue in
                                flight.standbyClearedAt = newValue
                                hasStandbyClearedAt = true
                                store.persist("update standby outcome")
                            }
                        }
                    }
                }

                VStack(alignment: .leading, spacing: 4) {
                    Text("NOTES")
                        .font(HistoryStyle.label(10))
                        .tracking(1.2)
                        .foregroundStyle(HistoryStyle.inkTertiary(scheme))
                    TextField(
                        "List position, jumpseat carrier, who cleared ahead of you…",
                        text: $standbyNotes,
                        axis: .vertical
                    )
                    .lineLimit(3, reservesSpace: true)
                    .font(.system(size: 13))
                    .foregroundStyle(HistoryStyle.ink(scheme))
                    .padding(10)
                    .background(HistoryStyle.cardSubtle(scheme), in: RoundedRectangle(cornerRadius: 10))
                    .onChange(of: standbyNotes) { _, newValue in
                        flight.standbyNotes = newValue.isEmpty ? nil : newValue
                        store.persist("update standby outcome")
                    }
                }
            }
            .padding(14)
            .historyCard(scheme, padding: 0)
        }
    }

    /// Pulls persisted standby fields onto our local @State so the
    /// editor reflects existing data. Missing dates default to the
    /// flight's scheduled departure (or now) so the DatePicker doesn't
    /// open on 2001-01-01.
    private func hydrateStandbyState() {
        standbyOutcome = flight.standbyOutcome ?? "confirmed"
        standbyNotes = flight.standbyNotes ?? ""

        let fallback = flight.scheduledDeparture ?? flight.flightDate
        if let attempted = flight.standbyAttemptedAt {
            standbyAttemptedAt = attempted
            hasStandbyAttemptedAt = true
        } else {
            standbyAttemptedAt = fallback
            hasStandbyAttemptedAt = false
        }
        if let cleared = flight.standbyClearedAt {
            standbyClearedAt = cleared
            hasStandbyClearedAt = true
        } else {
            standbyClearedAt = fallback
            hasStandbyClearedAt = false
        }
    }

    // MARK: - Airframe history

    /// Shows the user's personal history on this tail. Hidden when no
    /// registration is set (e.g. CSV imports) or when this is the only
    /// flight on the airframe — single-flight stats aren't interesting.
    @ViewBuilder
    private var airframeHistorySection: some View {
        if let reg = flight.registration, !reg.isEmpty,
           let stats = airframeStats, stats.totalFlights > 1 {
            VStack(alignment: .leading, spacing: 8) {
                HistorySectionLabel("Airframe history")
                VStack(spacing: 0) {
                    aircraftRow(
                        leftLabel: "Your flights",
                        leftValue: "\(stats.totalFlights)",
                        rightLabel: "Distinct routes",
                        rightValue: "\(stats.routes.count)"
                    )
                    divider
                    VStack(alignment: .leading, spacing: 4) {
                        Text("MOST COMMON ROUTE")
                            .font(HistoryStyle.label(10))
                            .tracking(1.3)
                            .foregroundStyle(HistoryStyle.inkTertiary(scheme))
                        Text(stats.mostCommonRoute ?? "—")
                            .font(.system(size: 14, weight: .heavy).monospaced())
                            .foregroundStyle(HistoryStyle.ink(scheme))
                    }
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .padding(14)
                }
                .historyCard(scheme, padding: 0)
            }
        }
    }

    private func loadAirframeHistory() {
        guard let reg = flight.registration, !reg.isEmpty else {
            airframeStats = nil
            return
        }
        airframeStats = airframeHistory.stats(forTail: reg, context: store.context)
    }

    // MARK: - Delete

    private var deleteButton: some View {
        Button(role: .destructive) {
            showDeleteConfirm = true
        } label: {
            HStack {
                Image(systemName: "trash")
                Text("Delete flight")
            }
            .font(.system(size: 14, weight: .semibold))
            .frame(maxWidth: .infinity)
            .padding(.vertical, 14)
        }
        .background(Color.red.opacity(0.12), in: RoundedRectangle(cornerRadius: 14))
        .foregroundStyle(Color.red)
        .padding(.top, 8)
    }

    // MARK: - Helpers

    private var durationDisplay: String {
        guard let min = store.durationMinutes(for: flight) else { return "—" }
        let h = min / 60
        let m = min % 60
        return h > 0 ? "\(h)h \(m)m" : "\(m)m"
    }

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

    private func years(since: Date) -> Int {
        Calendar.current.dateComponents([.year], from: since, to: Date()).year ?? 0
    }

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

    private func ordinalSuffix(_ n: Int) -> String {
        let r = n % 100
        if r >= 11 && r <= 13 { return "th" }
        switch n % 10 {
        case 1: return "st"
        case 2: return "nd"
        case 3: return "rd"
        default: return "th"
        }
    }

    private func longDate(_ d: Date) -> String {
        let f = DateFormatter(); f.dateFormat = "EEE, MMM d, yyyy"
        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)
    }

    private func shortTime(_ d: Date) -> String {
        let f = DateFormatter(); f.dateFormat = "h:mm a"
        return f.string(from: d)
    }
}

/// Map view used by the history detail. Draws the actual flown track
/// when supplied; otherwise a great-circle arc between dep + arr.
private struct FlightRouteMap: View {
    let departureIATA: String
    let arrivalIATA: String
    let track: AircraftTrack?
    let database: AirportDatabase

    var body: some View {
        let dep = database.airport(byIATA: departureIATA)?.coordinate
        let arr = database.airport(byIATA: arrivalIATA)?.coordinate
        let bearing = (dep != nil && arr != nil)
            ? HistoryDetailView.bearing(from: dep!, to: arr!)
            : 90

        Map {
            if let dep {
                Annotation("From " + departureIATA, coordinate: dep) {
                    routeMarker(systemName: "airplane.departure",
                                bearing: bearing,
                                tint: HistoryStyle.stampGreen)
                }
                .annotationTitles(.hidden)
            }
            if let arr {
                Annotation("To " + arrivalIATA, coordinate: arr) {
                    routeMarker(systemName: "airplane.arrival",
                                bearing: bearing,
                                tint: HistoryStyle.runwayOrange)
                }
                .annotationTitles(.hidden)
            }
            if let track {
                let coords = track.path.map {
                    CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude)
                }
                MapPolyline(coordinates: coords)
                    .stroke(HistoryStyle.runwayOrange, lineWidth: 3)
            } else if let dep = database.airport(byIATA: departureIATA),
                      let arr = database.airport(byIATA: arrivalIATA) {
                MapPolyline(coordinates: greatCircle(from: dep.coordinate, to: arr.coordinate, segments: 64))
                    .stroke(HistoryStyle.runwayOrange.opacity(0.7), style: StrokeStyle(lineWidth: 2, dash: [5, 4]))
            }
        }
    }

    /// Map-pin chrome for an airport endpoint. Wraps the SF symbol
    /// in a circle background, rotates the icon so it points in the
    /// actual flight direction (so a DAL→SAN flight shows planes
    /// pointing left, not the default right).
    @ViewBuilder
    private func routeMarker(systemName: String, bearing: Double, tint: Color) -> some View {
        Image(systemName: systemName)
            .font(.system(size: 14, weight: .black))
            .foregroundStyle(.white)
            // SF airplane.departure/airplane.arrival glyphs orient
            // roughly up-right at ~45°; rotation matches the central
            // route icon's correction.
            .rotationEffect(.degrees(bearing - 45))
            .frame(width: 30, height: 30)
            .background(tint, in: Circle())
            .overlay(Circle().stroke(.white, lineWidth: 1.5))
            .shadow(color: .black.opacity(0.4), radius: 2, y: 1)
    }

    private func greatCircle(from a: CLLocationCoordinate2D, to b: CLLocationCoordinate2D, segments: Int) -> [CLLocationCoordinate2D] {
        let lat1 = a.latitude * .pi / 180
        let lon1 = a.longitude * .pi / 180
        let lat2 = b.latitude * .pi / 180
        let lon2 = b.longitude * .pi / 180

        let d = 2 * asin(sqrt(
            pow(sin((lat2 - lat1) / 2), 2)
            + cos(lat1) * cos(lat2) * pow(sin((lon2 - lon1) / 2), 2)
        ))
        if d == 0 { return [a, b] }

        var out: [CLLocationCoordinate2D] = []
        out.reserveCapacity(segments + 1)
        for i in 0...segments {
            let f = Double(i) / Double(segments)
            let A = sin((1 - f) * d) / sin(d)
            let B = sin(f * d) / sin(d)
            let x = A * cos(lat1) * cos(lon1) + B * cos(lat2) * cos(lon2)
            let y = A * cos(lat1) * sin(lon1) + B * cos(lat2) * sin(lon2)
            let z = A * sin(lat1) + B * sin(lat2)
            let lat = atan2(z, sqrt(x * x + y * y))
            let lon = atan2(y, x)
            out.append(CLLocationCoordinate2D(latitude: lat * 180 / .pi, longitude: lon * 180 / .pi))
        }
        return out
    }
}
