import SwiftUI
import MapKit
import CoreLocation

/// Lifetime route map — redesigned.
///
/// What it does, in order:
///   1. On appear (and whenever filters change), the camera fits to
///      the bounding region of every filtered flight's dep + arr
///      airports with padding. You always see your data.
///   2. An animation auto-plays from oldest flight to newest. Each
///      flight gets a small plane icon that flies along the great-
///      circle from departure to arrival, drawing a solid orange
///      line behind it. The whole sweep takes ~4 seconds regardless
///      of flight count.
///   3. Airport dots are invisible at start. Each one pops in with a
///      brief pulse when its first flight lands there.
///   4. Drawn arcs stay drawn at full color; the most-recent flight
///      stays slightly brighter/thicker as a focal point.
///   5. Bottom drawer is a real swipe-up sheet with detents — peek
///      shows the count + replay; expanded shows filter chips.
struct HistoryRouteMapView: View {
    /// The user's full unfiltered log. The map filters this set
    /// internally by `filters` so changes from the in-map filter sheet
    /// reliably re-scope the animation, even if the parent's
    /// .sheet content closure doesn't propagate a new pre-filtered
    /// array to us in time.
    let allFlights: [LoggedFlight]
    let database: AirportDatabase
    let openSky: OpenSkyClient
    let store: FlightHistoryStore
    @Binding var filters: HistoryFilters

    /// Locally-derived "what to draw" set. Recomputed on every body
    /// pass; cheap because it's a single array filter on ~hundreds of
    /// items at most.
    private var flights: [LoggedFlight] {
        allFlights.filter { filters.matches($0) }
    }

    @State private var position: MapCameraPosition = .automatic
    @State private var progress: Double = 0
    @State private var animationKey: Int = 0
    @State private var schedule: AnimationSchedule = .empty
    @State private var selectedAirportSheet: AirportSheet?
    @State private var drawerExpanded: Bool = false
    @State private var dragOffset: CGFloat = 0
    @State private var showingFilterSheet: Bool = false

    /// Drawer heights — peek shows just the row of stats + replay,
    /// expanded shows filter chips + per-airport stats.
    private static let drawerPeekHeight: CGFloat = 96
    private static let drawerExpandedHeight: CGFloat = 340

    /// ~4 second total sweep, snappy. Per-flight slice is computed
    /// from this in `AnimationSchedule.build`.
    private static let totalDuration: TimeInterval = 4.0

    struct AirportSheet: Identifiable {
        let iata: String
        var id: String { iata }
    }

    var body: some View {
        ZStack(alignment: .bottom) {
            mapLayer
            // Overlay drawer (not a sheet) — so the parent sheet's
            // swipe-down-to-dismiss gesture still works on the map
            // surface. The drawer has two states: peek and expanded.
            // Tap or drag the handle to toggle; drag offset gives
            // live feedback during the gesture.
            mapDrawer
                .frame(maxWidth: .infinity)
                .frame(height: drawerHeight)
                .offset(y: dragOffset)
                .gesture(drawerDragGesture)
        }
        .navigationTitle(filters.isEmpty ? "Routes" : "Filtered Routes")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement: .primaryAction) {
                Button { showingFilterSheet = true } label: {
                    Image(systemName: filters.activeCount > 0
                          ? "line.3.horizontal.decrease.circle.fill"
                          : "line.3.horizontal.decrease.circle")
                }
            }
            ToolbarItem(placement: .primaryAction) {
                Button { restart() } label: {
                    Image(systemName: "arrow.clockwise.circle.fill")
                        .font(.title3)
                        .foregroundStyle(HistoryStyle.runwayOrange)
                }
            }
        }
        .onAppear { reset() }
        .onChange(of: filters) { _, _ in reset() }
        .task(id: animationKey) {
            await runAnimation()
        }
        .sheet(isPresented: $showingFilterSheet) {
            HistoryFilterSheet(allFlights: allFlights, filters: $filters)
                .presentationDetents([.medium, .large])
        }
        .sheet(item: $selectedAirportSheet) { sheet in
            NavigationStack {
                AirportFlightsView(
                    iata: sheet.iata,
                    allFlights: allFlights,
                    database: database,
                    store: store,
                    openSky: openSky,
                    filters: $filters
                )
            }
            .presentationDetents([.medium, .large])
        }
    }

    private var drawerHeight: CGFloat {
        drawerExpanded ? Self.drawerExpandedHeight : Self.drawerPeekHeight
    }

    /// Drag gesture for the drawer handle area.
    /// - Upward drag → expand
    /// - Downward drag (from expanded) → collapse to peek
    /// - dragOffset gives live feedback while the finger is down,
    ///   then snaps to a state on release with a spring.
    private var drawerDragGesture: some Gesture {
        DragGesture()
            .onChanged { value in
                let raw = value.translation.height
                // Clamp the live offset so the drawer can't be dragged
                // off-screen in either direction.
                if drawerExpanded {
                    dragOffset = max(0, raw)
                } else {
                    dragOffset = min(0, raw)
                }
            }
            .onEnded { value in
                let delta = value.translation.height
                withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
                    if drawerExpanded {
                        if delta > 60 { drawerExpanded = false }
                    } else {
                        if delta < -40 { drawerExpanded = true }
                    }
                    dragOffset = 0
                }
            }
    }

    // MARK: - Map

    private var mapLayer: some View {
        Map(position: $position) {
            // Completed + currently-animating arcs
            ForEach(schedule.segments) { seg in
                if let visibleCoords = seg.coordsVisible(at: progress) {
                    let isMostRecent = seg.id == schedule.mostRecentId
                    MapPolyline(coordinates: visibleCoords)
                        .stroke(
                            isMostRecent ? Color.yellow : HistoryStyle.runwayOrange,
                            style: StrokeStyle(
                                lineWidth: isMostRecent ? 3.0 : 2.0,
                                lineCap: .round,
                                lineJoin: .round
                            )
                        )
                }
            }

            // In-flight plane icons (only the segments currently animating).
            // The plane scales 0 → 1 → 0 across its flight: takes off
            // tiny, hits full size at the apex of the journey, lands
            // small again. Reads like a real plane disappearing into
            // the horizon and reappearing.
            ForEach(schedule.segments) { seg in
                if let head = seg.head(at: progress) {
                    let scale = seg.planeScale(at: progress)
                    Annotation("", coordinate: head.coord) {
                        Image(systemName: "airplane")
                            .font(.system(size: 16, weight: .black))
                            .foregroundStyle(.white)
                            .padding(5)
                            .background(HistoryStyle.runwayOrange, in: Circle())
                            .shadow(color: .black.opacity(0.4), radius: 3, y: 1)
                            .rotationEffect(.degrees(head.bearing - 90))
                            .scaleEffect(scale)
                            .opacity(scale)
                    }
                    .annotationTitles(.hidden)
                }
            }

            // Airports — pop in on first arrival
            ForEach(schedule.airports) { ap in
                if progress >= ap.litAt {
                    Annotation(ap.iata, coordinate: ap.coord) {
                        AirportPulseDot(
                            size: ap.dotSize,
                            timeSinceLit: progress - ap.litAt,
                            isSelected: filters.airports.contains(ap.iata)
                        )
                        .onTapGesture {
                            selectedAirportSheet = AirportSheet(iata: ap.iata)
                        }
                    }
                    .annotationTitles(.hidden)
                }
            }
        }
        .mapStyle(.standard(elevation: .flat, emphasis: .muted))
    }

    // MARK: - Bottom drawer (sheet)

    @ViewBuilder
    private var mapDrawer: some View {
        VStack(spacing: 0) {
            drawerHandle
            drawerPeek
            if drawerExpanded {
                Divider().opacity(0.5)
                drawerExpandedContent
            }
            Spacer(minLength: 0)
        }
        .background(
            UnevenRoundedRectangle(
                topLeadingRadius: 22,
                topTrailingRadius: 22
            )
            .fill(.regularMaterial)
            .ignoresSafeArea(edges: .bottom)
        )
        .shadow(color: .black.opacity(0.15), radius: 10, y: -2)
    }

    private var drawerHandle: some View {
        Capsule()
            .fill(.gray.opacity(0.4))
            .frame(width: 36, height: 5)
            .padding(.top, 8)
            .padding(.bottom, 4)
            .frame(maxWidth: .infinity)
            .contentShape(Rectangle())
            .onTapGesture {
                withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
                    drawerExpanded.toggle()
                }
            }
    }

    private var drawerPeek: some View {
        HStack(spacing: 14) {
            VStack(alignment: .leading, spacing: 1) {
                Text(filters.isEmpty ? "ALL TIME" : "FILTERED")
                    .font(.system(size: 10, weight: .heavy))
                    .tracking(2)
                    .foregroundStyle(HistoryStyle.runwayOrange)
                Text("\(flights.count) flights · \(totalMilesString)")
                    .font(.system(size: 17, weight: .heavy))
                    .foregroundStyle(.primary)
            }
            Spacer(minLength: 12)
            // Animation progress
            ZStack {
                Circle()
                    .stroke(.gray.opacity(0.18), lineWidth: 3)
                Circle()
                    .trim(from: 0, to: progress)
                    .stroke(HistoryStyle.runwayOrange,
                            style: StrokeStyle(lineWidth: 3, lineCap: .round))
                    .rotationEffect(.degrees(-90))
                if progress >= 1 {
                    Image(systemName: "checkmark")
                        .font(.system(size: 11, weight: .black))
                        .foregroundStyle(HistoryStyle.runwayOrange)
                }
            }
            .frame(width: 24, height: 24)

            Button { restart() } label: {
                Image(systemName: "arrow.clockwise")
                    .font(.system(size: 14, weight: .bold))
                    .foregroundStyle(.white)
                    .frame(width: 36, height: 36)
                    .background(HistoryStyle.runwayOrange, in: Circle())
            }
            .buttonStyle(.plain)
        }
        .padding(.horizontal, 18)
        .padding(.vertical, 12)
    }

    @ViewBuilder
    private var drawerExpandedContent: some View {
        VStack(alignment: .leading, spacing: 14) {
            if !filters.years.isEmpty || !filters.airlines.isEmpty || !filters.airports.isEmpty {
                Text("FILTERS")
                    .font(.system(size: 10, weight: .heavy))
                    .tracking(2)
                    .foregroundStyle(HistoryStyle.runwayOrange)
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(spacing: 6) {
                        ForEach(Array(filters.years).sorted(by: >), id: \.self) { y in
                            drawerChip("\(y)") { filters.years.remove(y) }
                        }
                        ForEach(Array(filters.airlines).sorted(), id: \.self) { a in
                            drawerChip(a) { filters.airlines.remove(a) }
                        }
                        ForEach(Array(filters.airports).sorted(), id: \.self) { a in
                            drawerChip(a) { filters.airports.remove(a) }
                        }
                    }
                }
            }

            // Quick stats grid
            HStack(spacing: 12) {
                statTile(label: "Airports", value: "\(schedule.airports.count)")
                statTile(label: "Routes", value: "\(uniqueRoutes)")
                statTile(label: "Years", value: "\(yearSpan)")
            }
            .padding(.top, 4)
            Spacer(minLength: 12)
        }
        .padding(.horizontal, 18)
        .padding(.vertical, 14)
    }

    private func drawerChip(_ label: String, onRemove: @escaping () -> Void) -> some View {
        HStack(spacing: 4) {
            Text(label).font(.system(size: 11, weight: .bold).monospaced())
            Image(systemName: "xmark").font(.system(size: 8, weight: .bold))
        }
        .foregroundStyle(.white)
        .padding(.horizontal, 10)
        .padding(.vertical, 5)
        .background(HistoryStyle.runwayOrange, in: Capsule())
        .onTapGesture(perform: onRemove)
    }

    private func statTile(label: String, value: String) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(label.uppercased())
                .font(.system(size: 10, weight: .heavy))
                .tracking(1.2)
                .foregroundStyle(.secondary)
            Text(value)
                .font(.system(size: 22, weight: .heavy).monospacedDigit())
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(12)
        .background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 12))
    }

    // MARK: - Derived

    private var uniqueRoutes: Int {
        Set(flights.map { [$0.departureIATA, $0.arrivalIATA].sorted().joined(separator: "↔") }).count
    }

    private var yearSpan: Int {
        let years = Set(flights.map { Calendar.current.component(.year, from: $0.flightDate) })
        return years.count
    }

    private var totalMilesString: String {
        let total = flights.reduce(0) { $0 + (store.distanceMiles(for: $1) ?? 0) }
        let f = NumberFormatter()
        f.numberStyle = .decimal
        return (f.string(from: NSNumber(value: total)) ?? "\(total)") + " mi"
    }

    // MARK: - Lifecycle

    /// Build the schedule and snap the camera to fit before the
    /// animation begins. Called on appear and on every filter change.
    private func reset() {
        schedule = AnimationSchedule.build(flights: flights, database: database, totalDuration: Self.totalDuration)
        if let region = schedule.fitRegion {
            position = .region(region)
        }
        progress = 0
        animationKey += 1
    }

    private func restart() {
        progress = 0
        animationKey += 1
    }

    /// Drive `progress` from 0 → 1 over `totalDuration` seconds. Uses
    /// the system clock so the animation finishes in real time even
    /// if a frame is dropped.
    private func runAnimation() async {
        let start = Date()
        let dur = Self.totalDuration
        while !Task.isCancelled {
            let elapsed = Date().timeIntervalSince(start)
            if elapsed >= dur {
                progress = 1
                return
            }
            progress = max(progress, elapsed / dur)
            // ~60fps tick
            try? await Task.sleep(nanoseconds: 16_666_666)
        }
    }
}

// MARK: - Animation schedule
//
// Plain value types. Built once per filter change. No I/O, no closure
// captures. Reads cleanly from the view's `body` because `coordsVisible`
// and `head` are O(1) sliced lookups against precomputed arrays.

struct AnimationSchedule {
    var segments: [FlightSegment] = []
    var airports: [AirportLight] = []
    var fitRegion: MKCoordinateRegion?
    var mostRecentId: UUID?

    static let empty = AnimationSchedule()

    /// Build the per-flight start/end progress windows and the per-
    /// airport "lights up at" progress from the user's filtered set.
    static func build(flights: [LoggedFlight], database: AirportDatabase, totalDuration: TimeInterval) -> AnimationSchedule {
        let sorted = flights.sorted { $0.flightDate < $1.flightDate }
        let resolved: [(flight: LoggedFlight, dep: CLLocationCoordinate2D, arr: CLLocationCoordinate2D)] =
            sorted.compactMap { f in
                guard let dep = database.airport(byIATA: f.departureIATA),
                      let arr = database.airport(byIATA: f.arrivalIATA)
                else { return nil }
                return (f, dep.coordinate, arr.coordinate)
            }
        guard !resolved.isEmpty else { return .empty }

        // Per-flight slice: we want a ~4s total with the planes
        // overlapping just enough that one starts before the previous
        // finishes (otherwise the map looks empty between flights).
        let n = Double(resolved.count)
        // Each plane traverses for `flightDur`. We stagger starts by
        // `(1 - flightDur) / (n - 1)` so the last finishes at 1.0.
        let flightDur: Double = min(0.18, max(0.06, 1.0 / max(n, 1.0) * 2.5))
        let stagger: Double = n > 1 ? (1.0 - flightDur) / (n - 1) : 0

        var segments: [FlightSegment] = []
        var firstArrivalForAirport: [String: Double] = [:]  // IATA → litAt progress
        var airportCounts: [String: Int] = [:]
        var airportCoords: [String: CLLocationCoordinate2D] = [:]

        for (i, item) in resolved.enumerated() {
            let startP = Double(i) * stagger
            let endP = startP + flightDur
            let coords = greatCircle(from: item.dep, to: item.arr, segments: 40)
            let seg = FlightSegment(
                id: UUID(),
                flightId: item.flight.id,
                coords: coords,
                startProgress: startP,
                endProgress: endP
            )
            segments.append(seg)

            // Departure lights up at startP (when its plane takes off);
            // arrival at endP (when the plane lands).
            let depIata = item.flight.departureIATA
            let arrIata = item.flight.arrivalIATA
            firstArrivalForAirport[depIata] = min(firstArrivalForAirport[depIata] ?? .infinity, startP)
            firstArrivalForAirport[arrIata] = min(firstArrivalForAirport[arrIata] ?? .infinity, endP)
            airportCounts[depIata, default: 0] += 1
            airportCounts[arrIata, default: 0] += 1
            airportCoords[depIata] = item.dep
            airportCoords[arrIata] = item.arr
        }

        let airports = firstArrivalForAirport.compactMap { iata, lit -> AirportLight? in
            guard let coord = airportCoords[iata] else { return nil }
            let count = airportCounts[iata] ?? 1
            return AirportLight(
                iata: iata,
                coord: coord,
                litAt: lit,
                dotSize: dotSizeFor(count: count)
            )
        }

        // Fit region — union of all dep + arr coordinates, padded.
        let coords = resolved.flatMap { [$0.dep, $0.arr] }
        let fit = boundingRegion(for: coords)

        return AnimationSchedule(
            segments: segments,
            airports: airports,
            fitRegion: fit,
            mostRecentId: segments.last?.id
        )
    }

    private static func dotSizeFor(count: Int) -> CGFloat {
        // log scale, 9pt → 22pt
        let v = log(Double(count) + 1) * 4 + 8
        return CGFloat(min(22, max(9, v)))
    }

    /// Bounding `MKCoordinateRegion` that fits all coords with padding.
    /// Adds ~20% margin to span so dots don't pin to the edges.
    private static func boundingRegion(for coords: [CLLocationCoordinate2D]) -> MKCoordinateRegion? {
        guard !coords.isEmpty else { return nil }
        let lats = coords.map { $0.latitude }
        let lons = coords.map { $0.longitude }
        let minLat = lats.min() ?? 0, maxLat = lats.max() ?? 0
        let minLon = lons.min() ?? 0, maxLon = lons.max() ?? 0
        let center = CLLocationCoordinate2D(
            latitude: (minLat + maxLat) / 2,
            longitude: (minLon + maxLon) / 2
        )
        let latDelta = max(0.5, (maxLat - minLat) * 1.4)
        let lonDelta = max(0.5, (maxLon - minLon) * 1.4)
        return MKCoordinateRegion(
            center: center,
            span: MKCoordinateSpan(latitudeDelta: latDelta, longitudeDelta: lonDelta)
        )
    }

    /// 41-point great-circle sample. MapKit doesn't draw GC paths
    /// natively, so we approximate with straight segments.
    private static 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
    }
}

/// One animatable flight segment. `coordsVisible` slices the pre-
/// computed great-circle array based on the global animation
/// progress; `head` gives the current plane position.
struct FlightSegment: Identifiable, Hashable {
    let id: UUID
    let flightId: UUID
    let coords: [CLLocationCoordinate2D]
    let startProgress: Double
    let endProgress: Double

    /// Returns the visible portion of the arc at the given global
    /// progress. nil if the flight hasn't started yet.
    func coordsVisible(at progress: Double) -> [CLLocationCoordinate2D]? {
        if progress < startProgress { return nil }
        if progress >= endProgress { return coords }
        let local = (progress - startProgress) / max(0.0001, endProgress - startProgress)
        let cutoff = max(1, Int(local * Double(coords.count - 1)) + 1)
        return Array(coords.prefix(cutoff))
    }

    struct Head {
        let coord: CLLocationCoordinate2D
        let bearing: Double
    }

    /// Returns the moving plane's current coordinate and travel
    /// bearing. nil when the flight isn't in-flight at this moment.
    func head(at progress: Double) -> Head? {
        guard progress >= startProgress, progress < endProgress else { return nil }
        let local = (progress - startProgress) / max(0.0001, endProgress - startProgress)
        let i = max(0, min(coords.count - 1, Int(local * Double(coords.count - 1))))
        let here = coords[i]
        let next = coords[min(coords.count - 1, i + 1)]
        return Head(coord: here, bearing: bearing(from: here, to: next))
    }

    /// Plane scale across the flight — 0 at takeoff, 1.0 at the
    /// midpoint, 0 at landing. Half-sine curve. Returns 0 when the
    /// flight isn't currently animating.
    func planeScale(at progress: Double) -> CGFloat {
        guard progress >= startProgress, progress < endProgress else { return 0 }
        let local = (progress - startProgress) / max(0.0001, endProgress - startProgress)
        return CGFloat(sin(local * .pi))
    }

    private 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)
    }

    func hash(into hasher: inout Hasher) { hasher.combine(id) }
    static func == (lhs: FlightSegment, rhs: FlightSegment) -> Bool { lhs.id == rhs.id }
}

/// One airport on the map. `litAt` is the progress value at which the
/// dot first appears (when the first plane to/from this airport
/// lands or takes off).
struct AirportLight: Identifiable, Hashable {
    let iata: String
    let coord: CLLocationCoordinate2D
    let litAt: Double
    let dotSize: CGFloat
    var id: String { iata }

    func hash(into hasher: inout Hasher) { hasher.combine(iata) }
    static func == (lhs: AirportLight, rhs: AirportLight) -> Bool { lhs.iata == rhs.iata }
}

// MARK: - Airport dot with light-up pulse

/// Tappable airport dot with a brief scale-up "pop" the first time it
/// appears, then settles. `timeSinceLit` is the global progress
/// elapsed since this airport first lit up, so we can drive a
/// transient pulse without per-dot @State.
private struct AirportPulseDot: View {
    let size: CGFloat
    let timeSinceLit: Double
    let isSelected: Bool

    /// Pulse window: bump scale up for ~0.05 progress (≈ 200ms at our
    /// 4s sweep), then ease back to 1.0.
    private var pulseScale: CGFloat {
        let window: Double = 0.05
        guard timeSinceLit < window else { return 1.0 }
        let frac = timeSinceLit / window
        // half-cosine ease — 1.6 at start, 1.0 at end
        let eased = 1.0 + 0.6 * cos(frac * .pi / 2)
        return CGFloat(eased)
    }

    var body: some View {
        Circle()
            .fill(isSelected ? Color.yellow : HistoryStyle.runwayOrange)
            .frame(width: size, height: size)
            .overlay(Circle().stroke(.white, lineWidth: 2))
            .shadow(color: .black.opacity(0.4), radius: 3, y: 1)
            .scaleEffect(pulseScale)
            .contentShape(Circle().inset(by: -10))
    }
}
