//
//  TripDetailView.swift
//  SportsTime
//

import SwiftUI
import SwiftData
import MapKit
import Combine
import UniformTypeIdentifiers

struct TripDetailView: View {
    @Environment(\.modelContext) private var modelContext
    @Environment(\.colorScheme) private var colorScheme
    @Environment(\.isDemoMode) private var isDemoMode

    let trip: Trip
    private let providedGames: [String: RichGame]?

    /// When true, shows Add buttons and custom items (only for saved trips)
    private let allowCustomItems: Bool

    @Query private var savedTrips: [SavedTrip]
    @State private var showProPaywall = false
    @State private var selectedDay: ItineraryDay?
    @State private var showExportSheet = false
    @State private var exportURL: URL?
    @State private var isExporting = false
    @State private var exportProgress: PDFAssetPrefetcher.PrefetchProgress?
    @State private var mapCameraPosition: MapCameraPosition = .automatic
    @State private var isSaved = false
    @State private var routeCoordinates: [[CLLocationCoordinate2D]] = []
    @State private var mapUpdateTrigger = UUID()  // Force map refresh
    @State private var isLoadingRoutes = false
    @State private var loadedGames: [String: RichGame] = [:]
    @State private var isLoadingGames = false
    @State private var hasAppliedDemoSelection = false

    // Itinerary items state
    @State private var itineraryItems: [ItineraryItem] = []
    @State private var addItemAnchor: AddItemAnchor?
    @State private var editingItem: ItineraryItem?
    @State private var subscriptionCancellable: AnyCancellable?
    @State private var draggedItem: ItineraryItem?
    @State private var draggedTravelId: String?  // Track which travel segment is being dragged
    @State private var dropTargetId: String?  // Track which drop zone is being hovered
    @State private var travelOverrides: [String: TravelOverride] = [:]  // Key: travel ID, Value: day + sortOrder

    // Apple Maps state
    @State private var showMultiRouteAlert = false
    @State private var multiRouteChunks: [[MKMapItem]] = []

    private let exportService = ExportService()
    private let dataProvider = AppDataProvider.shared

    /// Games dictionary - uses provided games if available, otherwise uses loaded games
    private var games: [String: RichGame] {
        providedGames ?? loadedGames
    }

    /// Initialize with trip and games dictionary (existing callers - no custom items)
    init(trip: Trip, games: [String: RichGame]) {
        self.trip = trip
        self.providedGames = games
        self.allowCustomItems = false
    }

    /// Initialize with just trip - games will be loaded from AppDataProvider (no custom items)
    init(trip: Trip) {
        self.trip = trip
        self.providedGames = nil
        self.allowCustomItems = false
    }

    /// Initialize for saved trip context - enables custom items
    init(trip: Trip, games: [String: RichGame], allowCustomItems: Bool) {
        self.trip = trip
        self.providedGames = games
        self.allowCustomItems = allowCustomItems
    }

    /// Initialize for saved trip context without provided games - loads from AppDataProvider
    init(trip: Trip, allowCustomItems: Bool) {
        self.trip = trip
        self.providedGames = nil
        self.allowCustomItems = allowCustomItems
    }

    var body: some View {
        bodyContent
    }

    @ViewBuilder
    private var bodyContent: some View {
        mainContent
            .background(Theme.backgroundGradient(colorScheme))
            .toolbarBackground(Theme.cardBackground(colorScheme), for: .navigationBar)
            .toolbar { toolbarContent }
            .modifier(SheetModifiers(
                showExportSheet: $showExportSheet,
                exportURL: exportURL,
                showProPaywall: $showProPaywall,
                addItemAnchor: $addItemAnchor,
                editingItem: $editingItem,
                tripId: trip.id,
                saveItineraryItem: saveItineraryItem
            ))
            .alert("Large Trip Route", isPresented: $showMultiRouteAlert) {
                ForEach(multiRouteChunks.indices, id: \.self) { index in
                    Button("Open Part \(index + 1) of \(multiRouteChunks.count)") {
                        AppleMapsLauncher.open(chunk: index, of: multiRouteChunks)
                    }
                }
                Button("Cancel", role: .cancel) { }
            } message: {
                Text("This trip has \(multiRouteChunks.flatMap { $0 }.count) stops, which exceeds Apple Maps' limit of 16. Open the route in parts?")
            }
            .onAppear {
                AnalyticsManager.shared.track(.tripViewed(tripId: trip.id.uuidString, source: allowCustomItems ? "saved" : "new"))
                checkIfSaved()
                // Demo mode: auto-favorite the trip
                if isDemoMode && !hasAppliedDemoSelection && !isSaved {
                    hasAppliedDemoSelection = true
                    DispatchQueue.main.asyncAfter(deadline: .now() + DemoConfig.selectionDelay + 0.5) {
                        if !isSaved {
                            saveTrip()
                        }
                    }
                }
            }
            .task {
                await loadGamesIfNeeded()
                if allowCustomItems {
                    await loadItineraryItems()
                    await setupSubscription()
                }
            }
            .onDisappear { subscriptionCancellable?.cancel() }
            .onChange(of: itineraryItems) { _, newItems in
                handleItineraryItemsChange(newItems)
            }
            .onChange(of: travelOverrides.count) { _, _ in
                draggedTravelId = nil
                dropTargetId = nil
            }
            .overlay {
                if isExporting { exportProgressOverlay }
            }
    }

    @ToolbarContentBuilder
    private var toolbarContent: some ToolbarContent {
        ToolbarItemGroup(placement: .primaryAction) {
            ShareButton(trip: trip, style: .icon)
                .foregroundStyle(Theme.warmOrange)

            Button {
                if StoreManager.shared.isPro {
                    Task { await exportPDF() }
                } else {
                    showProPaywall = true
                }
            } label: {
                HStack(spacing: 2) {
                    Image(systemName: "doc.fill")
                    if !StoreManager.shared.isPro {
                        ProBadge()
                    }
                }
                .foregroundStyle(Theme.warmOrange)
            }
            .accessibilityLabel("Export trip as PDF")
        }
    }

    private func handleItineraryItemsChange(_ newItems: [ItineraryItem]) {
        draggedItem = nil
        dropTargetId = nil
        print("🗺️ [MapUpdate] itineraryItems changed, count: \(newItems.count)")
        for item in newItems {
            if item.isCustom, let info = item.customInfo, info.isMappable {
                print("🗺️ [MapUpdate] Mappable: \(info.title) on day \(item.day), sortOrder: \(item.sortOrder)")
            }
        }
        Task {
            updateMapRegion()
            await fetchDrivingRoutes()
        }
    }

    // MARK: - Main Content

    @ViewBuilder
    private var mainContent: some View {
        if allowCustomItems {
            // Full-screen table with map as header
            ItineraryTableViewWrapper(
                trip: trip,
                games: Array(games.values),
                itineraryItems: itineraryItems,
                travelOverrides: travelOverrides,
                headerContent: {
                    VStack(spacing: 0) {
                        // Hero Map
                        heroMapSection
                            .frame(height: 280)

                        // Content header
                        VStack(spacing: Theme.Spacing.lg) {
                            tripHeader
                                .padding(.top, Theme.Spacing.lg)

                            statsRow

                            if let score = trip.score {
                                scoreCard(score)
                            }

                            // Itinerary title
                            Text("Itinerary")
                                .font(.title2)
                                .fontWeight(.bold)
                                .foregroundStyle(Theme.textPrimary(colorScheme))
                                .frame(maxWidth: .infinity, alignment: .leading)
                        }
                        .padding(.horizontal, Theme.Spacing.lg)
                        .padding(.bottom, Theme.Spacing.md)
                    }
                },
                onTravelMoved: { travelId, newDay, newSortOrder  in
                    Task { @MainActor in
                        withAnimation {
                            travelOverrides[travelId] = TravelOverride(day: newDay, sortOrder: newSortOrder)
                        }
                        await saveTravelDayOverride(travelAnchorId: travelId, displayDay: newDay, sortOrder: newSortOrder)
                    }
                },
                onCustomItemMoved: { itemId, day, sortOrder in
                    Task { @MainActor in
                        guard let item = itineraryItems.first(where: { $0.id == itemId }) else { return }
                        await moveItem(item, toDay: day, sortOrder: sortOrder)
                    }
                },
                onCustomItemTapped: { item in
                    editingItem = item
                },
                onCustomItemDeleted: { item in
                    Task { await deleteItineraryItem(item) }
                },
                onAddButtonTapped: { day in
                    addItemAnchor = AddItemAnchor(day: day)
                }
            )
            .ignoresSafeArea(edges: .bottom)
        } else {
            // Non-editable scroll view for unsaved trips
            ScrollView {
                VStack(spacing: 0) {
                    heroMapSection
                        .frame(height: 280)

                    VStack(spacing: Theme.Spacing.lg) {
                        tripHeader
                            .padding(.top, Theme.Spacing.lg)

                        statsRow

                        if let score = trip.score {
                            scoreCard(score)
                        }

                        itinerarySection
                    }
                    .padding(.horizontal, Theme.Spacing.lg)
                    .padding(.bottom, Theme.Spacing.xxl)
                }
                .onDrop(of: [.text, .plainText, .utf8PlainText], isTargeted: .constant(false)) { _ in
                    draggedTravelId = nil
                    draggedItem = nil
                    dropTargetId = nil
                    return true
                }
            }
        }
    }

    // MARK: - Export Progress Overlay

    private var exportProgressOverlay: some View {
        ZStack {
            // Background dimmer
            Color.black.opacity(0.6)
                .ignoresSafeArea()

            // Progress card
            VStack(spacing: Theme.Spacing.lg) {
                // Progress ring
                ZStack {
                    Circle()
                        .stroke(Theme.cardBackgroundElevated(colorScheme), lineWidth: 8)
                        .frame(width: 80, height: 80)

                    Circle()
                        .trim(from: 0, to: exportProgress?.percentComplete ?? 0)
                        .stroke(Theme.warmOrange, style: StrokeStyle(lineWidth: 8, lineCap: .round))
                        .frame(width: 80, height: 80)
                        .rotationEffect(.degrees(-90))
                        .animation(
                            Theme.Animation.prefersReducedMotion ? nil : .easeInOut(duration: 0.3),
                            value: exportProgress?.percentComplete
                        )

                    Image(systemName: "doc.fill")
                        .font(.title2)
                        .foregroundStyle(Theme.warmOrange)
                }

                VStack(spacing: Theme.Spacing.xs) {
                    Text("Creating PDF")
                        .font(.headline)
                        .foregroundStyle(Theme.textPrimary(colorScheme))

                    Text(exportProgress?.currentStep ?? "Preparing...")
                        .font(.subheadline)
                        .foregroundStyle(Theme.textSecondary(colorScheme))
                        .multilineTextAlignment(.center)

                    if let progress = exportProgress {
                        Text("\(Int(progress.percentComplete * 100))%")
                            .font(.caption.monospaced())
                            .foregroundStyle(Theme.textMuted(colorScheme))
                    }
                }
            }
            .padding(Theme.Spacing.xl)
            .background(Theme.cardBackground(colorScheme))
            .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
            .shadow(color: .black.opacity(0.3), radius: 20, y: 10)
        }
        .transition(.opacity)
    }

    // MARK: - Hero Map Section

    private var heroMapSection: some View {
        ZStack(alignment: .bottom) {
            TripMapView(
                cameraPosition: $mapCameraPosition,
                routeCoordinates: routeCoordinates,
                stopCoordinates: stopCoordinates,
                customItems: mappableCustomItems,
                colorScheme: colorScheme,
                routeVersion: mapUpdateTrigger
            )
            .id("map-\(mapUpdateTrigger)")
            .overlay(alignment: .topTrailing) {
                // Save/Unsave heart button
                Button {
                    toggleSaved()
                } label: {
                    Image(systemName: isSaved ? "heart.fill" : "heart")
                        .font(.title3)
                        .foregroundStyle(isSaved ? .red : .white)
                        .padding(12)
                        .background(.ultraThinMaterial)
                        .clipShape(Circle())
                        .shadow(color: .black.opacity(0.2), radius: 4, y: 2)
                }
                .accessibilityIdentifier("tripDetail.favoriteButton")
                .accessibilityLabel(isSaved ? "Remove from favorites" : "Save to favorites")
                .padding(.top, 12)
                .padding(.trailing, 12)
            }
            .overlay(alignment: .bottomTrailing) {
                // Open in Apple Maps button
                Button {
                    openInAppleMaps()
                } label: {
                    Image(systemName: "map.fill")
                        .font(.title3)
                        .foregroundStyle(.white)
                        .padding(12)
                        .background(Theme.warmOrange)
                        .clipShape(Circle())
                        .shadow(color: .black.opacity(0.3), radius: 4, y: 2)
                }
                .padding(.bottom, 90) // Above the gradient
                .padding(.trailing, 12)
                .accessibilityLabel("Open in Apple Maps")
                .accessibilityHint("Opens this trip route in Apple Maps")
            }

            // Gradient overlay at bottom
            LinearGradient(
                colors: [.clear, Theme.cardBackground(colorScheme).opacity(0.8), Theme.cardBackground(colorScheme)],
                startPoint: .top,
                endPoint: .bottom
            )
            .frame(height: 80)

            // Loading indicator
            if isLoadingRoutes {
                LoadingSpinner(size: .medium)
                    .padding(.bottom, 40)
            }
        }
        .task {
            updateMapRegion()
            await fetchDrivingRoutes()
        }
    }

    // MARK: - Header

    private var tripHeader: some View {
        VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
            // Date range
            Text(trip.formattedDateRange)
                .font(.subheadline)
                .foregroundStyle(Theme.textSecondary(colorScheme))

            // Route preview
            RoutePreviewStrip(cities: trip.stops.map { $0.city })
                .padding(.vertical, Theme.Spacing.xs)

            // Sport badges
            HStack(spacing: Theme.Spacing.xs) {
                ForEach(Array(trip.uniqueSports), id: \.self) { sport in
                    HStack(spacing: 4) {
                        Image(systemName: sport.iconName)
                            .font(.caption2)
                        Text(sport.rawValue)
                            .font(.caption2)
                    }
                    .padding(.horizontal, 10)
                    .padding(.vertical, 5)
                    .background(sport.themeColor.opacity(0.2))
                    .foregroundStyle(sport.themeColor)
                    .clipShape(Capsule())
                }
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    // MARK: - Stats Row

    private var statsRow: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: Theme.Spacing.sm) {
                StatPill(icon: "calendar", value: "\(trip.tripDuration) days")
                StatPill(icon: "mappin.circle", value: "\(trip.stops.count) cities")
                StatPill(icon: "sportscourt", value: "\(trip.totalGames) games")
                StatPill(icon: "road.lanes", value: trip.formattedTotalDistance)
                StatPill(icon: "car", value: trip.formattedTotalDriving)
            }
        }
    }

    // MARK: - Score Card

    private func scoreCard(_ score: TripScore) -> some View {
        VStack(spacing: Theme.Spacing.md) {
            HStack {
                Text("Trip Score")
                    .font(.headline)
                    .foregroundStyle(Theme.textPrimary(colorScheme))
                Spacer()
                Text(score.scoreGrade)
                    .font(.largeTitle)
                    .foregroundStyle(Theme.warmOrange)
                    .glowEffect(color: Theme.warmOrange, radius: 8)
            }

            HStack(spacing: Theme.Spacing.lg) {
                scoreItem(label: "Games", value: score.gameQualityScore, color: Theme.mlbRed)
                scoreItem(label: "Route", value: score.routeEfficiencyScore, color: Theme.routeGold)
                scoreItem(label: "Balance", value: score.leisureBalanceScore, color: Theme.mlsGreen)
                scoreItem(label: "Prefs", value: score.preferenceAlignmentScore, color: Theme.nbaOrange)
            }
        }
        .cardStyle()
    }

    private func scoreItem(label: String, value: Double, color: Color) -> some View {
        VStack(spacing: 4) {
            Text(String(format: "%.0f", value))
                .font(.headline)
                .foregroundStyle(color)
            Text(label)
                .font(.caption)
                .foregroundStyle(Theme.textMuted(colorScheme))
        }
    }

    // MARK: - Itinerary (for non-editable scroll view)

    private var itinerarySection: some View {
        VStack(alignment: .leading, spacing: Theme.Spacing.md) {
            Text("Itinerary")
                .font(.title2)
                .foregroundStyle(Theme.textPrimary(colorScheme))

            if isLoadingGames {
                HStack {
                    Spacer()
                    ProgressView("Loading games...")
                        .padding(.vertical, Theme.Spacing.xl)
                    Spacer()
                }
            } else {
                // Non-editable view for non-saved trips
                ZStack(alignment: .top) {
                    Rectangle()
                        .fill(Theme.routeGold.opacity(0.4))
                        .frame(width: 2)
                        .frame(maxHeight: .infinity)

                    VStack(spacing: Theme.Spacing.md) {
                        ForEach(Array(itinerarySections.enumerated()), id: \.offset) { index, section in
                            itineraryRow(for: section, at: index)
                        }
                    }
                }
            }
        }
    }

    @ViewBuilder
    private func itineraryRow(for section: ItinerarySection, at index: Int) -> some View {
        let sectionId = sectionIdentifier(for: section, at: index)
        let isDragging = draggedItem != nil || draggedTravelId != nil
        let isDropTarget = dropTargetId == sectionId && isDragging

        switch section {
        case .day(let dayNumber, let date, let gamesOnDay):
            // Show indicator at TOP for travel (travel appears above day), BOTTOM for custom items
            let indicatorAlignment: Alignment = draggedTravelId != nil ? .top : .bottom

            // Pre-compute if this day is a valid travel target
            let isValidTravelTarget: Bool = {
                guard let travelId = draggedTravelId,
                      let validRange = validDayRange(for: travelId) else { return true }
                return validRange.contains(dayNumber)
            }()

            DaySection(
                dayNumber: dayNumber,
                date: date,
                games: gamesOnDay
            )
            .staggeredAnimation(index: index)
            .overlay(alignment: indicatorAlignment) {
                // Only show indicator if valid target (or dragging custom item)
                if isDropTarget && (draggedTravelId == nil || isValidTravelTarget) {
                    DropTargetIndicator()
                }
            }
            .onDrop(of: [.text, .plainText, .utf8PlainText], isTargeted: Binding(
                get: { dropTargetId == sectionId },
                set: { targeted in
                    // Only show as target if it's a valid drop location
                    let shouldShowTarget = targeted && (draggedTravelId == nil || isValidTravelTarget)
                    Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
                        if shouldShowTarget {
                            dropTargetId = sectionId
                        } else if dropTargetId == sectionId {
                            dropTargetId = nil
                        }
                    }
                }
            )) { providers in
                handleDayDrop(providers: providers, dayNumber: dayNumber, gamesOnDay: gamesOnDay)
            }

        case .travel(let segment, let segmentIndex):
            let travelId = stableTravelAnchorId(segment, at: segmentIndex)
            TravelSection(segment: segment)
                .staggeredAnimation(index: index)
                .overlay(alignment: .bottom) {
                    // Show drop indicator for custom items, but not when dragging this travel
                    if isDropTarget && draggedTravelId != travelId {
                        DropTargetIndicator()
                    }
                }
                .onDrag {
                    draggedTravelId = travelId
                    return NSItemProvider(object: travelId as NSString)
                }
                .onDrop(of: [.text, .plainText, .utf8PlainText], isTargeted: Binding(
                    get: { dropTargetId == sectionId },
                    set: { targeted in
                    // Only accept custom items on travel, not other travel
                    let shouldShow = targeted && draggedItem != nil
                    Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
                        if shouldShow {
                            dropTargetId = sectionId
                        } else if dropTargetId == sectionId {
                            dropTargetId = nil
                            }
                        }
                    }
                )) { providers in
                    handleTravelDrop(providers: providers, segment: segment)
                }

        case .customItem(let item):
            let isDraggingThis = draggedItem?.id == item.id
            CustomItemRow(
                item: item,
                onTap: { editingItem = item }
            )
            .contextMenu {
                Button(role: .destructive) {
                    Task { await deleteItineraryItem(item) }
                } label: {
                    Label("Delete", systemImage: "trash")
                }
            }
            .opacity(isDraggingThis ? 0.4 : 1.0)
            .staggeredAnimation(index: index)
            .overlay(alignment: .top) {
                if isDropTarget && !isDraggingThis {
                    DropTargetIndicator()
                }
            }
            .onDrag {
                draggedItem = item
                return NSItemProvider(object: item.id.uuidString as NSString)
            }
            .onDrop(of: [.text, .plainText, .utf8PlainText], isTargeted: Binding(
                get: { dropTargetId == sectionId },
                set: { targeted in
                    // Only accept custom items, not travel
                    let shouldShow = targeted && draggedItem != nil && draggedItem?.id != item.id
                    Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
                        if shouldShow {
                            dropTargetId = sectionId
                        } else if dropTargetId == sectionId {
                            dropTargetId = nil
                        }
                    }
                }
            )) { providers in
                handleCustomItemDrop(providers: providers, targetItem: item)
            }

        case .addButton(let day):
            VStack(spacing: 0) {
                if isDropTarget {
                    DropTargetIndicator()
                }
                InlineAddButton {
                    addItemAnchor = AddItemAnchor(day: day)
                }
            }
            .onDrop(of: [.text, .plainText, .utf8PlainText], isTargeted: Binding(
                get: { dropTargetId == sectionId },
                set: { targeted in
                    // Only accept custom items, not travel
                    let shouldShow = targeted && draggedItem != nil
                    Theme.Animation.withMotion(.easeInOut(duration: 0.2)) {
                        if shouldShow {
                            dropTargetId = sectionId
                        } else if dropTargetId == sectionId {
                            dropTargetId = nil
                        }
                    }
                }
            )) { providers in
                handleAddButtonDrop(providers: providers, day: day)
            }
        }
    }

    // MARK: - Drop Handlers

    private func handleTravelDrop(providers: [NSItemProvider], segment: TravelSegment) -> Bool {
        guard let provider = providers.first, provider.canLoadObject(ofClass: NSString.self) else {
            return false
        }

        // Clear drag state immediately (synchronously) before async work
        draggedTravelId = nil
        draggedItem = nil
        dropTargetId = nil

        provider.loadObject(ofClass: NSString.self) { item, _ in
            guard let droppedId = item as? String,
                  let itemId = UUID(uuidString: droppedId),
                  let droppedItem = self.itineraryItems.first(where: { $0.id == itemId }) else { return }

            Task { @MainActor in
                let day = self.findDayForTravelSegment(segment)
                // Place at beginning of day (sortOrder before existing items)
                let minSortOrder = self.itineraryItems
                    .filter { $0.day == day && $0.id != droppedItem.id }
                    .map { $0.sortOrder }
                    .min() ?? 1.0
                await self.moveItem(droppedItem, toDay: day, sortOrder: minSortOrder / 2.0)
            }
        }
        return true
    }

    private func handleCustomItemDrop(providers: [NSItemProvider], targetItem: ItineraryItem) -> Bool {
        guard let provider = providers.first, provider.canLoadObject(ofClass: NSString.self) else {
            return false
        }

        // Clear drag state immediately (synchronously) before async work
        draggedTravelId = nil
        draggedItem = nil
        dropTargetId = nil

        provider.loadObject(ofClass: NSString.self) { item, _ in
            guard let droppedId = item as? String,
                  let itemId = UUID(uuidString: droppedId),
                  let droppedItem = self.itineraryItems.first(where: { $0.id == itemId }),
                  droppedItem.id != targetItem.id else { return }

            Task { @MainActor in
                // Place before target item using midpoint insertion
                let itemsInDay = self.itineraryItems.filter { $0.day == targetItem.day && $0.id != droppedItem.id }
                    .sorted { $0.sortOrder < $1.sortOrder }
                let targetIdx = itemsInDay.firstIndex(where: { $0.id == targetItem.id }) ?? 0
                let prevSortOrder = targetIdx > 0 ? itemsInDay[targetIdx - 1].sortOrder : 0.0
                let newSortOrder = (prevSortOrder + targetItem.sortOrder) / 2.0
                await self.moveItem(droppedItem, toDay: targetItem.day, sortOrder: newSortOrder)
            }
        }
        return true
    }

    private func handleAddButtonDrop(providers: [NSItemProvider], day: Int) -> Bool {
        guard let provider = providers.first, provider.canLoadObject(ofClass: NSString.self) else {
            return false
        }

        // Clear drag state immediately (synchronously) before async work
        draggedTravelId = nil
        draggedItem = nil
        dropTargetId = nil

        provider.loadObject(ofClass: NSString.self) { item, _ in
            guard let droppedId = item as? String,
                  let itemId = UUID(uuidString: droppedId),
                  let droppedItem = self.itineraryItems.first(where: { $0.id == itemId }) else { return }

            Task { @MainActor in
                // Calculate sortOrder: append at end of day's items
                let maxSortOrder = self.itineraryItems
                    .filter { $0.day == day && $0.id != droppedItem.id }
                    .map { $0.sortOrder }
                    .max() ?? 0.0
                await self.moveItem(droppedItem, toDay: day, sortOrder: maxSortOrder + 1.0)
            }
        }
        return true
    }

    private func clearDragState() {
        draggedItem = nil
        draggedTravelId = nil
        dropTargetId = nil
    }

    /// Create a stable identifier for an itinerary section (for drop target tracking)
    private func sectionIdentifier(for section: ItinerarySection, at index: Int) -> String {
        switch section {
        case .day(let dayNumber, _, _):
            return "day-\(dayNumber)"
        case .travel(let segment, let segmentIndex):
            return "travel-\(segmentIndex)-\(segment.fromLocation.name)-\(segment.toLocation.name)"
        case .customItem(let item):
            return "item-\(item.id.uuidString)"
        case .addButton(let day):
            return "add-\(day)"
        }
    }

    private func findDayForTravelSegment(_ segment: TravelSegment) -> Int {
        // Find which day this travel segment belongs to by looking at sections
        // Travel appears BEFORE the arrival day, so look FORWARD to find arrival day
        for (index, section) in itinerarySections.enumerated() {
            if case .travel(let s, _) = section, s.id == segment.id {
                // Look forward to find the arrival day
                for i in (index + 1)..<itinerarySections.count {
                    if case .day(let dayNumber, _, _) = itinerarySections[i] {
                        return dayNumber
                    }
                }
            }
        }
        return 1
    }

    /// Create a stable anchor ID for a travel segment (UUIDs regenerate on reload)
    private func stableTravelAnchorId(_ segment: TravelSegment, at index: Int) -> String {
        let from = segment.fromLocation.name.lowercased().trimmingCharacters(in: .whitespaces)
        let to = segment.toLocation.name.lowercased().trimmingCharacters(in: .whitespaces)
        return "travel:\(index):\(from)->\(to)"
    }

    /// Move item to a new day and sortOrder position
    private func moveItem(_ item: ItineraryItem, toDay day: Int, sortOrder: Double) async {
        var updated = item
        updated.day = day
        updated.sortOrder = sortOrder
        updated.modifiedAt = Date()

        let title = item.customInfo?.title ?? "item"
        print("📍 [Move] Moving \(title) to day \(day), sortOrder: \(sortOrder)")

        // Update local state
        if let idx = itineraryItems.firstIndex(where: { $0.id == item.id }) {
            itineraryItems[idx] = updated
        }

        // Sync to CloudKit (debounced)
        await ItineraryItemService.shared.updateItem(updated)
        print("✅ [Move] Synced \(title) with day: \(day), sortOrder: \(sortOrder)")
    }

    /// Build itinerary sections: shows ALL days with travel, custom items, and add buttons
    private var itinerarySections: [ItinerarySection] {
        var sections: [ItinerarySection] = []
        let days = tripDays

        // Use TravelPlacement for consistent day calculation (shared with tests).
        var travelByDay = TravelPlacement.computeTravelByDay(trip: trip, tripDays: days)

        // Apply user overrides on top of computed defaults.
        for (segmentIndex, segment) in trip.travelSegments.enumerated() {
            let travelId = stableTravelAnchorId(segment, at: segmentIndex)
            guard let override = travelOverrides[travelId] else { continue }

            // Validate override is within valid day range
            if let validRange = validDayRange(for: travelId),
               validRange.contains(override.day) {
                // Remove from computed position (search all days)
                for key in travelByDay.keys {
                    travelByDay[key]?.removeAll { $0.id == segment.id }
                }
                travelByDay.keys.forEach { key in
                    if travelByDay[key]?.isEmpty == true { travelByDay[key] = nil }
                }
                // Place at overridden position
                travelByDay[override.day, default: []].append(segment)
            }
        }

        // Process ALL days
        for (index, dayDate) in days.enumerated() {
            let dayNum = index + 1
            let gamesOnDay = gamesOn(date: dayDate)

            // Travel for this day (if any) - appears before day header
            for travelSegment in (travelByDay[dayNum] ?? []) {
                let segIdx = trip.travelSegments.firstIndex(where: { $0.id == travelSegment.id }) ?? 0
                sections.append(.travel(travelSegment, segmentIndex: segIdx))
            }

            // Day section - shows games or minimal rest day display
            sections.append(.day(dayNumber: dayNum, date: dayDate, games: gamesOnDay))

            // Custom items for this day (sorted by sortOrder)
            if allowCustomItems {
                // Add button first - always right after day header
                sections.append(.addButton(day: dayNum))

                let dayItems = itineraryItems
                    .filter { $0.day == dayNum && $0.isCustom }
                    .sorted { $0.sortOrder < $1.sortOrder }
                for item in dayItems {
                    sections.append(.customItem(item))
                }
            }
        }

        return sections
    }

    private var tripDays: [Date] {
        let calendar = Calendar.current
        guard let startDate = trip.stops.first?.arrivalDate,
              let endDate = trip.stops.last?.departureDate else { return [] }

        var days: [Date] = []
        var current = calendar.startOfDay(for: startDate)
        let end = calendar.startOfDay(for: endDate)

        while current <= end {
            days.append(current)
            current = calendar.date(byAdding: .day, value: 1, to: current)!
        }
        return days
    }

    private func gamesOn(date: Date) -> [RichGame] {
        let calendar = Calendar.current
        let dayStart = calendar.startOfDay(for: date)
        let allGameIds = trip.stops.flatMap { $0.games }

        let foundGames = allGameIds.compactMap { games[$0] }

        return foundGames.filter { richGame in
            calendar.startOfDay(for: richGame.game.dateTime) == dayStart
        }.sorted { $0.game.dateTime < $1.game.dateTime }
    }

    /// Get the city for a given date (from the stop that covers that date)
    private func cityOn(date: Date) -> String? {
        let calendar = Calendar.current
        let dayStart = calendar.startOfDay(for: date)

        return trip.stops.first { stop in
            let arrivalDay = calendar.startOfDay(for: stop.arrivalDate)
            let departureDay = calendar.startOfDay(for: stop.departureDate)
            return dayStart >= arrivalDay && dayStart <= departureDay
        }?.city
    }

    /// Convert a date to a 1-based day number within the trip.
    /// Returns 0 if the date is before the trip, or tripDays.count + 1 if after.
    private func dayNumber(for date: Date) -> Int {
        let calendar = Calendar.current
        let target = calendar.startOfDay(for: date)
        let days = tripDays

        for (index, tripDay) in days.enumerated() {
            if calendar.startOfDay(for: tripDay) == target {
                return index + 1
            }
        }

        // Date is outside the trip range
        if let firstDay = days.first, target < firstDay {
            return 0
        }
        return days.count + 1
    }

    /// Get valid day range for a travel segment using stop indices.
    /// Uses the from/to stop dates so repeat cities don't confuse placement.
    private func validDayRange(for travelId: String) -> ClosedRange<Int>? {
        // Parse segment index from travel ID (format: "travel:INDEX:from->to")
        guard let segmentIndex = Self.parseSegmentIndex(from: travelId),
              segmentIndex < trip.stops.count - 1 else {
            return nil
        }

        let fromStop = trip.stops[segmentIndex]
        let toStop = trip.stops[segmentIndex + 1]

        let fromDayNum = dayNumber(for: fromStop.departureDate)
        let toDayNum = dayNumber(for: toStop.arrivalDate)

        let minDay = max(fromDayNum + 1, 1)
        let maxDay = min(toDayNum, tripDays.count)

        if minDay > maxDay {
            return nil
        }

        return minDay...maxDay
    }

    /// Parse the segment index from a travel anchor ID.
    /// Format: "travel:INDEX:from->to" → INDEX
    private static func parseSegmentIndex(from travelId: String) -> Int? {
        let stripped = travelId.replacingOccurrences(of: "travel:", with: "")
        let parts = stripped.components(separatedBy: ":")
        guard parts.count >= 2, let index = Int(parts[0]) else { return nil }
        return index
    }

    // MARK: - Map Helpers

    private func fetchDrivingRoutes() async {
        // Use routeWaypoints which includes game stops + mappable custom items
        let waypoints = routeWaypoints
        print("🗺️ [FetchRoutes] Computing routes with \(waypoints.count) waypoints:")
        for (index, wp) in waypoints.enumerated() {
            print("🗺️ [FetchRoutes]   \(index): \(wp.name) (custom: \(wp.isCustomItem))")
        }
        guard waypoints.count >= 2 else {
            print("🗺️ [FetchRoutes] Not enough waypoints, skipping")
            return
        }

        isLoadingRoutes = true
        var allCoordinates: [[CLLocationCoordinate2D]] = []

        for i in 0..<(waypoints.count - 1) {
            let source = waypoints[i]
            let destination = waypoints[i + 1]

            let request = MKDirections.Request()
            let sourceLocation = CLLocation(latitude: source.coordinate.latitude, longitude: source.coordinate.longitude)
            let destLocation = CLLocation(latitude: destination.coordinate.latitude, longitude: destination.coordinate.longitude)
            request.source = MKMapItem(location: sourceLocation, address: nil)
            request.destination = MKMapItem(location: destLocation, address: nil)
            request.transportType = .automobile

            let directions = MKDirections(request: request)

            do {
                let response = try await directions.calculate()
                if let route = response.routes.first {
                    // Extract coordinates from MKPolyline
                    let polyline = route.polyline
                    let pointCount = polyline.pointCount
                    var coords: [CLLocationCoordinate2D] = []
                    let points = polyline.points()
                    for j in 0..<pointCount {
                        coords.append(points[j].coordinate)
                    }
                    allCoordinates.append(coords)
                }
            } catch {
                // Fallback to straight line if directions unavailable
                allCoordinates.append([source.coordinate, destination.coordinate])
            }
        }

        await MainActor.run {
            print("🗺️ [FetchRoutes] Setting \(allCoordinates.count) route segments")
            routeCoordinates = allCoordinates
            mapUpdateTrigger = UUID()  // Force map to re-render with new routes
            isLoadingRoutes = false
        }
    }

    private var stopCoordinates: [(name: String, coordinate: CLLocationCoordinate2D)] {
        trip.stops.compactMap { stop -> (String, CLLocationCoordinate2D)? in
            if let coord = stop.coordinate {
                return (stop.city, coord)
            }
            if let stadiumId = stop.stadium,
               let stadium = dataProvider.stadium(for: stadiumId) {
                return (stadium.name, stadium.coordinate)
            }
            return nil
        }
    }

    /// Mappable custom items for display on the map
    private var mappableCustomItems: [ItineraryItem] {
        itineraryItems.filter { $0.isCustom && $0.customInfo?.isMappable == true }
    }

    /// Convert stored route coordinates to MKPolyline for rendering
    private var routePolylinesFromCoords: [MKPolyline] {
        routeCoordinates.map { coords in
            MKPolyline(coordinates: coords, count: coords.count)
        }
    }

    /// Route waypoints including both game stops and mappable custom items in itinerary order
    private var routeWaypoints: [(name: String, coordinate: CLLocationCoordinate2D, isCustomItem: Bool)] {
        // Build an ordered list combining game stops and mappable custom items
        // Items are ordered by (day, sortOrder) - visual order matches route order
        let itemsByDay = Dictionary(grouping: mappableCustomItems) { $0.day }

        print("🗺️ [Waypoints] Building waypoints. Mappable items by day:")
        for (day, items) in itemsByDay.sorted(by: { $0.key < $1.key }) {
            for item in items.sorted(by: { $0.sortOrder < $1.sortOrder }) {
                let title = item.customInfo?.title ?? "item"
                print("🗺️ [Waypoints]   Day \(day): \(title), sortOrder: \(item.sortOrder)")
            }
        }

        var waypoints: [(name: String, coordinate: CLLocationCoordinate2D, isCustomItem: Bool)] = []
        let days = tripDays

        print("🗺️ [Waypoints] Trip has \(days.count) days")

        for (dayIndex, dayDate) in days.enumerated() {
            let dayNumber = dayIndex + 1

            // Find games on this day
            let gamesOnDay = gamesOn(date: dayDate)
            let calendar = Calendar.current
            let dayCity = gamesOnDay.first?.stadium.city ?? trip.stops.first(where: { stop in
                let arrival = calendar.startOfDay(for: stop.arrivalDate)
                let departure = calendar.startOfDay(for: stop.departureDate)
                let day = calendar.startOfDay(for: dayDate)
                return day >= arrival && day <= departure
            })?.city

            print("🗺️ [Waypoints] Day \(dayNumber): city=\(dayCity ?? "none"), games=\(gamesOnDay.count)")

            // Game stop for this day (only add once per city to avoid duplicates)
            if let city = dayCity {
                // Check if we already have this city in waypoints (by city name or stadium name)
                let alreadyHasCity = waypoints.contains(where: { wp in
                    if wp.isCustomItem { return false }
                    // Check by city name
                    if wp.name == city { return true }
                    // Check by stadium name for this city
                    if let stop = trip.stops.first(where: { $0.city == city }),
                       let stadiumId = stop.stadium,
                       let stadium = dataProvider.stadium(for: stadiumId),
                       wp.name == stadium.name { return true }
                    return false
                })

                if !alreadyHasCity {
                    if let stop = trip.stops.first(where: { $0.city == city }) {
                        if let stadiumId = stop.stadium,
                           let stadium = dataProvider.stadium(for: stadiumId) {
                            print("🗺️ [Waypoints]   Adding \(stadium.name) (stadium)")
                            waypoints.append((stadium.name, stadium.coordinate, false))
                        } else if let coord = stop.coordinate {
                            // No stadium ID but stop has coordinate
                            print("🗺️ [Waypoints]   Adding \(city) (city coord)")
                            waypoints.append((city, coord, false))
                        }
                    }
                } else {
                    print("🗺️ [Waypoints]   \(city) already in waypoints, skipping")
                }
            }

            // Custom items for this day (ordered by sortOrder - visual order matches route)
            if let items = itemsByDay[dayNumber] {
                let sortedItems = items.sorted { $0.sortOrder < $1.sortOrder }
                for item in sortedItems {
                    if let info = item.customInfo, let coord = info.coordinate {
                        print("🗺️ [Waypoints]   Adding \(info.title) (sortOrder: \(item.sortOrder))")
                        waypoints.append((info.title, coord, true))
                    }
                }
            }
        }

        return waypoints
    }

    private func updateMapRegion() {
        // Include both game stops and mappable custom items in region calculation
        let waypoints = routeWaypoints
        guard !waypoints.isEmpty else { return }

        let coordinates = waypoints.map(\.coordinate)
        let lats = coordinates.map(\.latitude)
        let lons = coordinates.map(\.longitude)

        guard let minLat = lats.min(),
              let maxLat = lats.max(),
              let minLon = lons.min(),
              let maxLon = lons.max() else { return }

        let center = CLLocationCoordinate2D(
            latitude: (minLat + maxLat) / 2,
            longitude: (minLon + maxLon) / 2
        )

        let latSpan = (maxLat - minLat) * 1.3 + 0.5
        let lonSpan = (maxLon - minLon) * 1.3 + 0.5

        mapCameraPosition = .region(MKCoordinateRegion(
            center: center,
            span: MKCoordinateSpan(latitudeDelta: max(latSpan, 1), longitudeDelta: max(lonSpan, 1))
        ))
    }

    // MARK: - Actions

    /// Load games from AppDataProvider if not provided
    private func loadGamesIfNeeded() async {
        // Skip if games were provided
        guard providedGames == nil else { return }

        // Collect all game IDs from the trip
        let gameIds = trip.stops.flatMap { $0.games }
        guard !gameIds.isEmpty else { return }

        isLoadingGames = true

        // Load RichGame data from AppDataProvider
        var loaded: [String: RichGame] = [:]
        for gameId in gameIds {
            do {
                if let game = try await dataProvider.fetchGame(by: gameId),
                   let richGame = dataProvider.richGame(from: game) {
                    loaded[gameId] = richGame
                }
            } catch {
                // Skip games that fail to load
            }
        }

        loadedGames = loaded
        isLoadingGames = false
    }

    private func exportPDF() async {
        isExporting = true
        exportProgress = nil
        AnalyticsManager.shared.track(.pdfExportStarted(tripId: trip.id.uuidString, stopCount: trip.stops.count))

        do {
            // Build complete itinerary items (games + travel + custom)
            let completeItems = buildCompleteItineraryItems()

            let url = try await exportService.exportToPDF(
                trip: trip,
                games: games,
                itineraryItems: completeItems
            ) { progress in
                await MainActor.run {
                    self.exportProgress = progress
                }
            }
            exportURL = url
            showExportSheet = true
            AnalyticsManager.shared.track(.pdfExportCompleted(tripId: trip.id.uuidString))
        } catch {
            AnalyticsManager.shared.track(.pdfExportFailed(tripId: trip.id.uuidString, error: error.localizedDescription))
        }

        isExporting = false
    }

    /// Build complete itinerary items by merging games, travel, and custom items
    private func buildCompleteItineraryItems() -> [ItineraryItem] {
        var allItems: [ItineraryItem] = []

        // Get itinerary days from trip
        let tripDays = trip.itineraryDays()

        // 1. Add game items using day.gameIds (reliable source from trip stops)
        for day in tripDays {
            for (gameIndex, gameId) in day.gameIds.enumerated() {
                guard let richGame = games[gameId] else { continue }
                let gameItem = ItineraryItem(
                    tripId: trip.id,
                    day: day.dayNumber,
                    sortOrder: Double(gameIndex) * 0.01,  // Games near the start of the day
                    kind: .game(gameId: gameId, city: richGame.stadium.city)
                )
                allItems.append(gameItem)
            }
        }

        // 2. Add travel items (from trip segments + overrides)
        for (segmentIndex, segment) in trip.travelSegments.enumerated() {
            let travelId = stableTravelAnchorId(segment, at: segmentIndex)

            // Use override if available, otherwise default to day 1
            let override = travelOverrides[travelId]
            let day = override?.day ?? 1
            let sortOrder = override?.sortOrder ?? 100.0  // After games by default

            let travelItem = ItineraryItem(
                tripId: trip.id,
                day: day,
                sortOrder: sortOrder,
                kind: .travel(TravelInfo(
                    fromCity: segment.fromLocation.name,
                    toCity: segment.toLocation.name,
                    segmentIndex: segmentIndex,
                    distanceMeters: segment.distanceMeters,
                    durationSeconds: segment.durationSeconds
                ))
            )
            allItems.append(travelItem)
        }

        // 3. Add custom items (from CloudKit)
        let customItems = itineraryItems.filter { $0.isCustom }
        allItems.append(contentsOf: customItems)

        return allItems
    }

    private func toggleSaved() {
        if isSaved {
            unsaveTrip()
        } else {
            saveTrip()
        }
    }

    private func openInAppleMaps() {
        let result = AppleMapsLauncher.prepare(
            stops: trip.stops,
            customItems: mappableCustomItems
        )

        switch result {
        case .ready(let mapItems):
            AppleMapsLauncher.open(mapItems)

        case .multipleRoutes(let chunks):
            multiRouteChunks = chunks
            showMultiRouteAlert = true

        case .noWaypoints:
            // No routable locations - button shouldn't be visible but handle gracefully
            break
        }
    }

    private func saveTrip() {
        // Check trip limit for free users
        if !StoreManager.shared.isPro && savedTrips.count >= StoreManager.freeTripLimit {
            showProPaywall = true
            return
        }

        guard let savedTrip = SavedTrip.from(trip, games: games, status: .planned) else {
            return
        }

        modelContext.insert(savedTrip)

        do {
            try modelContext.save()
            Theme.Animation.withMotion(.spring(response: 0.3, dampingFraction: 0.6)) {
                isSaved = true
            }
            AnalyticsManager.shared.track(.tripSaved(
                tripId: trip.id.uuidString,
                stopCount: trip.stops.count,
                gameCount: trip.totalGames
            ))
        } catch {
            // Save failed silently
        }
    }

    private func unsaveTrip() {
        let tripId = trip.id
        let descriptor = FetchDescriptor<SavedTrip>(
            predicate: #Predicate { $0.id == tripId }
        )

        do {
            let savedTrips = try modelContext.fetch(descriptor)
            for savedTrip in savedTrips {
                modelContext.delete(savedTrip)
            }
            try modelContext.save()
            Theme.Animation.withMotion(.spring(response: 0.3, dampingFraction: 0.6)) {
                isSaved = false
            }
            AnalyticsManager.shared.track(.tripDeleted(tripId: tripId.uuidString))
        } catch {
            // Unsave failed silently
        }
    }

    private func checkIfSaved() {
        let tripId = trip.id
        let descriptor = FetchDescriptor<SavedTrip>(
            predicate: #Predicate { $0.id == tripId }
        )

        if let count = try? modelContext.fetchCount(descriptor), count > 0 {
            isSaved = true
        } else {
            isSaved = false
        }
    }

    // MARK: - Itinerary Items (CloudKit persistence)

    private func setupSubscription() async {
        // TODO: Re-implement CloudKit subscription for ItineraryItem changes
        // The subscription service was removed during the ItineraryItem refactor.
        // For now, items are only loaded on view appear.
        print("📡 [Subscription] CloudKit subscriptions not yet implemented for ItineraryItem")
    }

    private func loadItineraryItems() async {
        print("🔍 [ItineraryItems] Loading items for trip: \(trip.id)")
        do {
            let items = try await ItineraryItemService.shared.fetchItems(forTripId: trip.id)
            print("✅ [ItineraryItems] Loaded \(items.count) items from CloudKit")
            itineraryItems = items

            // Extract travel overrides (day + sortOrder) from travel-type items
            var overrides: [String: TravelOverride] = [:]

            for item in items where item.isTravel {
                guard let travelInfo = item.travelInfo else { continue }
                let from = travelInfo.fromCity.lowercased()
                let to = travelInfo.toCity.lowercased()

                if let segIdx = travelInfo.segmentIndex {
                    // New format with segment index
                    let travelId = "travel:\(segIdx):\(from)->\(to)"
                    overrides[travelId] = TravelOverride(day: item.day, sortOrder: item.sortOrder)
                } else {
                    // Legacy record without segment index — derive index from trip segments
                    if let segIdx = trip.travelSegments.firstIndex(where: {
                        $0.fromLocation.name.lowercased() == from && $0.toLocation.name.lowercased() == to
                    }) {
                        let travelId = "travel:\(segIdx):\(from)->\(to)"
                        overrides[travelId] = TravelOverride(day: item.day, sortOrder: item.sortOrder)
                    }
                }
            }

            travelOverrides = overrides
            print("✅ [TravelOverrides] Extracted \(overrides.count) travel overrides (day + sortOrder)")
        } catch {
            print("❌ [ItineraryItems] Failed to load: \(error)")
        }
    }

    private func saveItineraryItem(_ item: ItineraryItem) async {
        // Check if this is an update or create
        let isUpdate = itineraryItems.contains(where: { $0.id == item.id })
        let title = item.customInfo?.title ?? "item"
        print("💾 [ItineraryItems] Saving item: '\(title)' (isUpdate: \(isUpdate))")
        print("   - tripId: \(item.tripId)")
        print("   - day: \(item.day), sortOrder: \(item.sortOrder)")

        // Update local state immediately for responsive UI
        if isUpdate {
            if let index = itineraryItems.firstIndex(where: { $0.id == item.id }) {
                itineraryItems[index] = item
            }
        } else {
            itineraryItems.append(item)
        }

        // Persist to CloudKit
        do {
            if isUpdate {
                await ItineraryItemService.shared.updateItem(item)
                print("✅ [ItineraryItems] Updated in CloudKit: \(title)")
            } else {
                _ = try await ItineraryItemService.shared.createItem(item)
                print("✅ [ItineraryItems] Created in CloudKit: \(title)")
            }
        } catch {
            print("❌ [ItineraryItems] CloudKit save failed: \(error)")
        }
    }

    private func deleteItineraryItem(_ item: ItineraryItem) async {
        let title = item.customInfo?.title ?? "item"
        print("🗑️ [ItineraryItems] Deleting item: '\(title)'")

        // Remove from local state immediately
        itineraryItems.removeAll { $0.id == item.id }

        // Delete from CloudKit
        do {
            try await ItineraryItemService.shared.deleteItem(item.id)
            print("✅ [ItineraryItems] Deleted from CloudKit")
        } catch {
            print("❌ [ItineraryItems] CloudKit delete failed: \(error)")
        }
    }

    private func handleDayDrop(providers: [NSItemProvider], dayNumber: Int, gamesOnDay: [RichGame]) -> Bool {
        guard let provider = providers.first else { return false }

        // Capture and clear drag state immediately (synchronously) before async work
        // This ensures the UI resets even if validation fails
        let capturedTravelId = draggedTravelId
        let capturedItem = draggedItem
        draggedTravelId = nil
        draggedItem = nil
        dropTargetId = nil

        // Load the string from the provider
        if provider.canLoadObject(ofClass: NSString.self) {
            provider.loadObject(ofClass: NSString.self) { item, _ in
                guard let droppedId = item as? String else { return }

                Task { @MainActor in
                    // Check if this is a travel segment being dropped
                    if droppedId.hasPrefix("travel:") {
                        // Validate travel is within valid bounds (day-level)
                        if let validRange = self.validDayRange(for: droppedId) {
                            guard validRange.contains(dayNumber) else {
                                return
                            }
                        }
                        
                        // Choose a semantic sortOrder for dropping onto a day:
                        // - If this day has games, default to AFTER games (positive)
                        // - If no games, default to 1.0
                        //
                        // You can later support "before games" drops by using a negative sortOrder
                        // when the user drops above the games row.
                        let maxSortOrderOnDay = self.itineraryItems
                            .filter { $0.day == dayNumber }
                            .map { $0.sortOrder }
                            .max() ?? 0.0
                        
                        let newSortOrder = max(maxSortOrderOnDay + 1.0, 1.0)
                        
                        withAnimation {
                            self.travelOverrides[droppedId] = TravelOverride(day: dayNumber, sortOrder: newSortOrder)
                        }
                        
                        // Persist to CloudKit as a travel ItineraryItem
                        await self.saveTravelDayOverride(
                            travelAnchorId: droppedId,
                            displayDay: dayNumber,
                            sortOrder: newSortOrder
                        )
                        return
                    }

                    // Otherwise, it's a custom item drop
                    guard let itemId = UUID(uuidString: droppedId),
                          let item = self.itineraryItems.first(where: { $0.id == itemId }) else { return }

                    // Append at end of day's items
                    let maxSortOrder = self.itineraryItems
                        .filter { $0.day == dayNumber && $0.id != item.id }
                        .map { $0.sortOrder }
                        .max() ?? 0.0
                    await self.moveItem(item, toDay: dayNumber, sortOrder: maxSortOrder + 1.0)
                }
            }
            return true
        }
        return false
    }

    private func saveTravelDayOverride(travelAnchorId: String, displayDay: Int, sortOrder: Double) async {
        print("💾 [TravelOverrides] Saving override: \(travelAnchorId) -> day \(displayDay), sortOrder \(sortOrder)")

        // Parse travel ID (format: "travel:INDEX:from->to")
        let segmentIndex = Self.parseSegmentIndex(from: travelAnchorId)
        let stripped = travelAnchorId.replacingOccurrences(of: "travel:", with: "")
        let colonParts = stripped.components(separatedBy: ":")
        // After removing "travel:", format is "INDEX:from->to"
        let cityPart = colonParts.count >= 2 ? colonParts.dropFirst().joined(separator: ":") : stripped
        let cityParts = cityPart.components(separatedBy: "->")
        guard cityParts.count == 2 else {
            print("❌ [TravelOverrides] Invalid travel ID format: \(travelAnchorId)")
            return
        }

        let fromCity = cityParts[0]
        let toCity = cityParts[1]

        // Find existing travel item matching by segment index (preferred) or city pair (legacy)
        if let existingIndex = itineraryItems.firstIndex(where: {
            guard $0.isTravel, let info = $0.travelInfo else { return false }
            // Match by segment index if available
            if let idx = segmentIndex, let itemIdx = info.segmentIndex {
                return idx == itemIdx
            }
            // Legacy fallback: match by city pair
            return info.fromCity.lowercased() == fromCity && info.toCity.lowercased() == toCity
        }) {
            // Update existing
            var updated = itineraryItems[existingIndex]
            updated.day = displayDay
            updated.sortOrder = sortOrder
            updated.modifiedAt = Date()
            itineraryItems[existingIndex] = updated
            await ItineraryItemService.shared.updateItem(updated)
        } else {
            // Create new travel item
            let travelInfo = TravelInfo(fromCity: fromCity, toCity: toCity, segmentIndex: segmentIndex)
            let item = ItineraryItem(
                tripId: trip.id,
                day: displayDay,
                sortOrder: sortOrder,
                kind: .travel(travelInfo)
            )
            itineraryItems.append(item)
            do {
                _ = try await ItineraryItemService.shared.createItem(item)
            } catch {
                print("❌ [TravelOverrides] CloudKit save failed: \(error)")
                return
            }
        }
        print("✅ [TravelOverrides] Saved to CloudKit")
    }
}

// MARK: - Itinerary Section

enum ItinerarySection {
    case day(dayNumber: Int, date: Date, games: [RichGame])
    case travel(TravelSegment, segmentIndex: Int)
    case customItem(ItineraryItem)
    case addButton(day: Int)

    var isCustomItem: Bool {
        if case .customItem = self { return true }
        return false
    }
}

// MARK: - Add Item Anchor (for sheet)

struct AddItemAnchor: Identifiable {
    let id = UUID()
    let day: Int
}

// MARK: - Inline Add Button

private struct InlineAddButton: View {
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            HStack {
                Image(systemName: "plus.circle.fill")
                    .foregroundStyle(Theme.warmOrange.opacity(0.6))
                Text("Add")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
            .padding(.vertical, 4)
            .padding(.horizontal, 8)
        }
        .buttonStyle(.plain)
        .frame(maxWidth: .infinity, alignment: .leading)
    }
}

// MARK: - Drop Target Indicator

private struct DropTargetIndicator: View {
    var body: some View {
        HStack(spacing: 6) {
            Circle()
                .fill(Theme.warmOrange)
                .frame(width: 8, height: 8)
            Rectangle()
                .fill(Theme.warmOrange)
                .frame(height: 2)
            Circle()
                .fill(Theme.warmOrange)
                .frame(width: 8, height: 8)
        }
        .padding(.horizontal, Theme.Spacing.md)
        .padding(.vertical, 4)
        .transition(.opacity.combined(with: .scale(scale: 0.8)))
    }
}

// MARK: - Day Section

struct DaySection: View {
    let dayNumber: Int
    let date: Date
    let games: [RichGame]
    @Environment(\.colorScheme) private var colorScheme

    private var formattedDate: String {
        date.formatted(.dateTime.weekday(.wide).month().day())
    }

    private var gameCity: String? {
        games.first?.stadium.city
    }

    private var isRestDay: Bool {
        games.isEmpty
    }

    var body: some View {
        if isRestDay {
            // Minimal rest day display - just header with date
            HStack {
                VStack(alignment: .leading, spacing: 2) {
                    Text("Day \(dayNumber)")
                        .font(.title2)
                        .foregroundStyle(Theme.textPrimary(colorScheme))

                    Text(formattedDate)
                        .font(.subheadline)
                        .foregroundStyle(Theme.textSecondary(colorScheme))
                }

                Spacer()
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .cardStyle()
        } else {
            // Full game day display
            VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
                // Day header
                HStack {
                    VStack(alignment: .leading, spacing: 2) {
                        Text("Day \(dayNumber)")
                            .font(.title2)
                            .foregroundStyle(Theme.textPrimary(colorScheme))

                        Text(formattedDate)
                            .font(.subheadline)
                            .foregroundStyle(Theme.textSecondary(colorScheme))
                    }

                    Spacer()

                    Text("\(games.count) game\(games.count > 1 ? "s" : "")")
                        .badgeStyle(color: Theme.warmOrange, filled: false)
                }

                // City label
                if let city = gameCity {
                    Label(city, systemImage: "mappin")
                        .font(.subheadline)
                        .foregroundStyle(Theme.textSecondary(colorScheme))
                }

                // Games
                ForEach(games, id: \.game.id) { richGame in
                    GameRow(game: richGame)
                }
            }
            .cardStyle()
        }
    }
}

// MARK: - Game Row

struct GameRow: View {
    let game: RichGame
    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        HStack(spacing: Theme.Spacing.md) {
            // Sport color bar
            SportColorBar(sport: game.game.sport)

            VStack(alignment: .leading, spacing: 4) {
                // Sport badge + Matchup
                HStack(spacing: 6) {
                    // Sport icon and name
                    HStack(spacing: 3) {
                        Image(systemName: game.game.sport.iconName)
                            .font(.caption2)
                        Text(game.game.sport.rawValue)
                            .font(.caption2)
                    }
                    .foregroundStyle(game.game.sport.themeColor)

                    // Matchup
                    HStack(spacing: 4) {
                        Text(game.awayTeam.abbreviation)
                            .font(.body)
                        Text("@")
                            .foregroundStyle(Theme.textMuted(colorScheme))
                        Text(game.homeTeam.abbreviation)
                            .font(.body)
                    }
                    .foregroundStyle(Theme.textPrimary(colorScheme))
                }

                // Stadium
                HStack(spacing: 4) {
                    Image(systemName: "building.2")
                        .font(.caption2)
                    Text(game.stadium.name)
                        .font(.subheadline)
                }
                .foregroundStyle(Theme.textSecondary(colorScheme))
            }

            Spacer()

            // Time
            Text(game.localGameTimeShort)
                .font(.subheadline)
                .foregroundStyle(Theme.warmOrange)
        }
        .padding(Theme.Spacing.sm)
        .background(Theme.cardBackgroundElevated(colorScheme))
        .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.small))
    }
}

// MARK: - Travel Section

struct TravelSection: View {
    let segment: TravelSegment
    @Environment(\.colorScheme) private var colorScheme
    @State private var showEVChargers = false

    private var hasEVChargers: Bool {
        !segment.evChargingStops.isEmpty
    }

    var body: some View {
        // Travel card
        VStack(spacing: 0) {
                // Main travel info
                HStack(spacing: Theme.Spacing.md) {
                    // Icon
                    ZStack {
                        Circle()
                            .fill(Theme.cardBackgroundElevated(colorScheme))
                            .frame(width: 44, height: 44)

                        Image(systemName: "car.fill")
                            .foregroundStyle(Theme.routeGold)
                    }

                    VStack(alignment: .leading, spacing: 2) {
                        Text("Travel")
                            .font(.caption)
                            .foregroundStyle(Theme.textMuted(colorScheme))

                        Text("\(segment.fromLocation.name) → \(segment.toLocation.name)")
                            .font(.body)
                            .foregroundStyle(Theme.textPrimary(colorScheme))
                    }

                    Spacer()

                    VStack(alignment: .trailing, spacing: 2) {
                        Text(segment.formattedDistance)
                            .font(.subheadline)
                            .foregroundStyle(Theme.textPrimary(colorScheme))
                        Text(segment.formattedDuration)
                            .font(.caption)
                            .foregroundStyle(Theme.textSecondary(colorScheme))
                    }
                }
                .padding(Theme.Spacing.md)

                // EV Chargers section (if available)
                if hasEVChargers {
                    Divider()
                        .background(Theme.routeGold.opacity(0.2))

                    Button {
                        Theme.Animation.withMotion(.spring(response: 0.3, dampingFraction: 0.8)) {
                            showEVChargers.toggle()
                        }
                    } label: {
                        HStack(spacing: Theme.Spacing.sm) {
                            Image(systemName: "bolt.fill")
                                .foregroundStyle(.green)
                                .font(.caption)

                            Text("\(segment.evChargingStops.count) EV Charger\(segment.evChargingStops.count > 1 ? "s" : "") Along Route")
                                .font(.subheadline)
                                .foregroundStyle(Theme.textPrimary(colorScheme))

                            Spacer()

                            Image(systemName: showEVChargers ? "chevron.up" : "chevron.down")
                                .font(.caption)
                                .foregroundStyle(Theme.textMuted(colorScheme))
                                .accessibilityHidden(true)
                        }
                        .padding(.horizontal, Theme.Spacing.md)
                        .padding(.vertical, Theme.Spacing.sm)
                        .contentShape(Rectangle())
                    }
                    .buttonStyle(.plain)

                    if showEVChargers {
                        VStack(spacing: 0) {
                            ForEach(segment.evChargingStops) { charger in
                                EVChargerRow(charger: charger)
                            }
                        }
                        .padding(.horizontal, Theme.Spacing.md)
                        .padding(.bottom, Theme.Spacing.sm)
                        .transition(.opacity.combined(with: .move(edge: .top)))
                    }
                }
            }
            .background {
                // Opaque base + semi-transparent accent (prevents line showing through)
                RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
                    .fill(Theme.cardBackground(colorScheme))
                RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
                    .fill(Theme.routeGold.opacity(0.05))
            }
            .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
            .overlay {
                RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
                    .strokeBorder(Theme.routeGold.opacity(0.3), lineWidth: 1)
            }
    }
}

// MARK: - EV Charger Row

struct EVChargerRow: View {
    let charger: EVChargingStop
    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        HStack(spacing: Theme.Spacing.sm) {
            // Connector line indicator
            VStack(spacing: 0) {
                Rectangle()
                    .fill(.green.opacity(0.3))
                    .frame(width: 1, height: 8)
                Circle()
                    .fill(.green)
                    .frame(width: 6, height: 6)
                Rectangle()
                    .fill(.green.opacity(0.3))
                    .frame(width: 1, height: 8)
            }

            VStack(alignment: .leading, spacing: 2) {
                HStack(spacing: Theme.Spacing.xs) {
                    Text(charger.name)
                        .font(.subheadline)
                        .foregroundStyle(Theme.textPrimary(colorScheme))
                        .lineLimit(1)

                    chargerTypeBadge
                }

                HStack(spacing: Theme.Spacing.xs) {
                    if let address = charger.location.address {
                        Text(address)
                            .font(.caption)
                            .foregroundStyle(Theme.textSecondary(colorScheme))
                    }

                    Text("•")
                        .foregroundStyle(Theme.textMuted(colorScheme))

                    Text("~\(charger.formattedChargeTime) charge")
                        .font(.caption)
                        .foregroundStyle(Theme.textSecondary(colorScheme))
                }
            }

            Spacer()
        }
        .padding(.vertical, 6)
    }

    @ViewBuilder
    private var chargerTypeBadge: some View {
        let (text, color) = chargerTypeInfo
        Text(text)
            .font(.caption2)
            .foregroundStyle(color)
            .padding(.horizontal, 6)
            .padding(.vertical, 2)
            .background(color.opacity(0.15))
            .clipShape(Capsule())
    }

    private var chargerTypeInfo: (String, Color) {
        switch charger.chargerType {
        case .supercharger:
            return ("Supercharger", .red)
        case .dcFast:
            return ("DC Fast", .blue)
        case .level2:
            return ("Level 2", .green)
        }
    }
}

// MARK: - Share Sheet

struct ShareSheet: UIViewControllerRepresentable {
    let items: [Any]

    func makeUIViewController(context: Context) -> UIActivityViewController {
        UIActivityViewController(activityItems: items, applicationActivities: nil)
    }

    func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}

// MARK: - Trip Map View (separate component for proper state updates)

struct TripMapView: View {
    @Binding var cameraPosition: MapCameraPosition
    let routeCoordinates: [[CLLocationCoordinate2D]]
    let stopCoordinates: [(name: String, coordinate: CLLocationCoordinate2D)]
    let customItems: [ItineraryItem]
    let colorScheme: ColorScheme
    let routeVersion: UUID  // Force re-render when routes change

    /// Create unique ID for each route segment based on start/end coordinates
    private func routeId(for coords: [CLLocationCoordinate2D], index: Int) -> String {
        guard let first = coords.first, let last = coords.last else {
            return "route-\(index)-empty"
        }
        return "route-\(index)-\(first.latitude)-\(first.longitude)-\(last.latitude)-\(last.longitude)"
    }

    var body: some View {
        let _ = print("🗺️ [TripMapView] Rendering with \(routeCoordinates.count) route segments, version: \(routeVersion)")
        Map(position: $cameraPosition, interactionModes: []) {
            // Routes (driving directions)
            ForEach(Array(routeCoordinates.enumerated()), id: \.offset) { index, coords in
                let _ = print("🗺️ [TripMapView] Drawing route \(index) with \(coords.count) points")
                if !coords.isEmpty {
                    MapPolyline(MKPolyline(coordinates: coords, count: coords.count))
                        .stroke(Theme.routeGold, lineWidth: 4)
                }
            }

            // Game stop markers
            ForEach(stopCoordinates.indices, id: \.self) { index in
                let stop = stopCoordinates[index]
                Annotation(stop.name, coordinate: stop.coordinate) {
                    PulsingDot(color: index == 0 ? Theme.warmOrange : Theme.routeGold, size: 10)
                }
            }

            // Custom item markers
            ForEach(customItems, id: \.id) { item in
                if let info = item.customInfo, let coordinate = info.coordinate {
                    Annotation(info.title, coordinate: coordinate) {
                        ZStack {
                            Circle()
                                .fill(Theme.warmOrange)
                                .frame(width: 24, height: 24)
                            Text(info.icon)
                                .font(.caption2)
                        }
                    }
                }
            }
        }
        .id(routeVersion)  // Force Map to recreate when routes change
        .mapStyle(colorScheme == .dark ? .standard(elevation: .flat, emphasis: .muted) : .standard)
    }
}

#Preview {
    NavigationStack {
        TripDetailView(
            trip: Trip(
                name: "MLB Road Trip",
                preferences: TripPreferences(
                    startLocation: LocationInput(name: "New York"),
                    endLocation: LocationInput(name: "Chicago")
                )
            )
        )
    }
}

// MARK: - Travel Override

struct TravelOverride: Equatable {
    let day: Int
    let sortOrder: Double
}

// MARK: - Sheet Modifiers

private struct SheetModifiers: ViewModifier {
    @Binding var showExportSheet: Bool
    let exportURL: URL?
    @Binding var showProPaywall: Bool
    @Binding var addItemAnchor: AddItemAnchor?
    @Binding var editingItem: ItineraryItem?
    let tripId: UUID
    let saveItineraryItem: (ItineraryItem) async -> Void

    func body(content: Content) -> some View {
        content
            .sheet(isPresented: $showExportSheet) {
                if let url = exportURL {
                    ShareSheet(items: [url])
                }
            }
            .sheet(isPresented: $showProPaywall) {
                PaywallView(source: "trip_detail")
            }
            .sheet(item: $addItemAnchor) { anchor in
                QuickAddItemSheet(
                    tripId: tripId,
                    day: anchor.day,
                    existingItem: nil
                ) { item in
                    Task { await saveItineraryItem(item) }
                }
            }
            .sheet(item: $editingItem) { item in
                QuickAddItemSheet(
                    tripId: tripId,
                    day: item.day,
                    existingItem: item
                ) { updatedItem in
                    Task { await saveItineraryItem(updatedItem) }
                }
            }
    }
}
