//
//  ItineraryTableViewController.swift
//  SportsTime
//
//  Native UITableViewController for fluid itinerary drag-and-drop reordering.
//
//  ═══════════════════════════════════════════════════════════════════════════════
//  ARCHITECTURE OVERVIEW
//  ═══════════════════════════════════════════════════════════════════════════════
//
//  WHY UITableViewController INSTEAD OF SWIFTUI?
//  ─────────────────────────────────────────────
//  SwiftUI's drag-and-drop APIs (`.draggable()`, `.dropDestination()`) have significant
//  limitations for complex reordering:
//
//  1. No real-time visual feedback during drag (item snaps only on drop)
//  2. Limited control over drop target validation during drag
//  3. Difficult to constrain items to valid drop zones while dragging
//  4. ForEach with onMove doesn't support heterogeneous item types well
//
//  UITableViewController provides:
//  - Native drag handles with familiar iOS reordering UX
//  - Real-time row shifting during drag via `targetIndexPathForMoveFromRowAt`
//  - Fine-grained control over valid drop positions
//  - Built-in animation and haptic feedback
//
//  The trade-off: We use UIHostingConfiguration to render SwiftUI views in cells,
//  getting UIKit's interaction model with SwiftUI's declarative UI.
//
//  ═══════════════════════════════════════════════════════════════════════════════
//  DATA FLOW
//  ═══════════════════════════════════════════════════════════════════════════════
//
//  ┌─────────────────────────────────────────────────────────────────────────────┐
//  │                         TripDetailView (SwiftUI)                            │
//  │  - Owns trip data, custom items, travel day overrides                       │
//  │  - Computes itinerary sections for non-table display                        │
//  └──────────────────────────────────┬──────────────────────────────────────────┘
//                                     │
//                                     ▼
//  ┌─────────────────────────────────────────────────────────────────────────────┐
//  │                    ItineraryTableViewWrapper (SwiftUI)                      │
//  │  - UIViewControllerRepresentable bridge                                     │
//  │  - buildItineraryData() transforms trip → [ItineraryDayData]               │
//  │  - Computes travelValidRanges for each travel segment                       │
//  │  - Passes callbacks for move/tap/delete events                              │
//  └──────────────────────────────────┬──────────────────────────────────────────┘
//                                     │
//                                     ▼
//  ┌─────────────────────────────────────────────────────────────────────────────┐
//  │               ItineraryTableViewController (UIKit)                          │
//  │  - reloadData() flattens [ItineraryDayData] → [ItineraryRowItem]           │
//  │  - Renders rows, handles drag-and-drop, fires callbacks                     │
//  └─────────────────────────────────────────────────────────────────────────────┘
//
//  ═══════════════════════════════════════════════════════════════════════════════
//  ROW ORDERING (FLATTENING STRATEGY)
//  ═══════════════════════════════════════════════════════════════════════════════
//
//  Each day is flattened into rows in this EXACT order:
//
//  1. TRAVEL (if arriving this day)     - "Detroit → Milwaukee" card
//  2. DAY HEADER + ADD BUTTON           - "Day 3 · Sunday, Mar 8    + Add"
//  3. GAMES                             - All games for this day (city label + cards)
//  4. CUSTOM ITEMS                      - User-added items sorted by sortOrder
//
//  NOTE: The Add button is EMBEDDED in the day header row (not a separate row).
//  This prevents items from being dragged between the header and Add button.
//
//  Visual example:
//  ┌─────────────────────────────────────┐
//  │ 🚗 Detroit → Milwaukee (327mi)      │  ← Travel (arrives Day 3)
//  ├─────────────────────────────────────┤
//  │ Day 3 · Sunday, Mar 8      + Add    │  ← Day Header with Add button
//  ├─────────────────────────────────────┤
//  │ Milwaukee                           │  ← Games (city + cards)
//  │ ┌─────────────────────────────────┐ │
//  │ │ NBA: ORL @ MIL    8:00 PM       │ │
//  │ └─────────────────────────────────┘ │
//  ├─────────────────────────────────────┤
//  │ 🍽️ Waffle House                     │  ← Custom Item (sortOrder: 1.0)
//  │ 🍽️ Joe's BBQ                        │  ← Custom Item (sortOrder: 2.0)
//  └─────────────────────────────────────┘
//
//  ═══════════════════════════════════════════════════════════════════════════════
//  REORDERABLE VS FIXED ITEMS
//  ═══════════════════════════════════════════════════════════════════════════════
//
//  REORDERABLE (user can drag):
//  - Travel segments (constrained to valid day range)
//  - Custom items (can move to any day, any position)
//
//  FIXED (no drag handle):
//  - Day headers with Add button (structural anchors)
//  - Games (determined by trip planning, not user-movable)
//
//  ═══════════════════════════════════════════════════════════════════════════════
//  TRAVEL MOVEMENT CONSTRAINTS
//  ═══════════════════════════════════════════════════════════════════════════════
//
//  Travel segments have HARD constraints on which days they can occupy.
//  This prevents impossible itineraries (e.g., arriving before leaving).
//
//  Valid range calculation (in ItineraryTableViewWrapper.buildItineraryData):
//
//  For travel "Detroit → Milwaukee":
//  - EARLIEST day: day after last game in Detroit (must finish games first)
//  - LATEST day: day of first game in Milwaukee (must arrive for game)
//
//  Example: If Detroit has games on Days 1-2, Milwaukee has game on Day 4:
//  - Valid range: Days 3-4 (can travel on Day 3 rest day, or Day 4 morning)
//
//  During drag, `targetIndexPathForMoveFromRowAt` clamps the drop position
//  to keep travel within its valid range. User sees travel "snap" to valid
//  positions rather than being allowed in invalid spots.
//
//  ═══════════════════════════════════════════════════════════════════════════════
//  SORT ORDER SYSTEM
//  ═══════════════════════════════════════════════════════════════════════════════
//
//  Custom items use a Double sortOrder for positioning within a day.
//  This allows unlimited insertions without integer collisions.
//
//  MIDPOINT INSERTION ALGORITHM (calculateSortOrder):
//
//  When dropping between two items:
//  - Item A has sortOrder 1.0
//  - Item B has sortOrder 2.0
//  - Dropped item gets sortOrder 1.5 (midpoint)
//
//  Edge cases:
//  - Dropping first: sortOrder = existing_first / 2.0
//  - Dropping last: sortOrder = existing_last + 1.0
//  - Empty day: sortOrder = 1.0
//
//  This allows unlimited reordering without needing to renumber all items.
//  Even after many insertions, precision remains sufficient (Double has ~15
//  significant digits, so you'd need millions of insertions to cause issues).
//
//  ═══════════════════════════════════════════════════════════════════════════════
//  CALLBACKS TO PARENT
//  ═══════════════════════════════════════════════════════════════════════════════
//
//  onTravelMoved: ((String, Int) -> Void)?
//    - Called when user drops travel segment
//    - Parameters: travelId (stable identifier), newDay (1-indexed)
//    - Parent stores override in travelDayOverrides dictionary
//
//  onCustomItemMoved: ((UUID, Int, Double) -> Void)?
//    - Called when user drops custom item
//    - Parameters: itemId, newDay (1-indexed), newSortOrder
//    - Parent updates item and syncs to CloudKit
//
//  onCustomItemTapped: ((ItineraryItem) -> Void)?
//    - Called when user taps custom item row
//    - Parent presents edit sheet
//
//  onCustomItemDeleted: ((ItineraryItem) -> Void)?
//    - Called from context menu delete action
//    - Parent deletes from CloudKit
//
//  onAddButtonTapped: ((Int) -> Void)?
//    - Called when user taps Add button
//    - Parameter: day number (1-indexed)
//    - Parent presents AddItemSheet for that day
//
//  ═══════════════════════════════════════════════════════════════════════════════
//  KEY IMPLEMENTATION DETAILS
//  ═══════════════════════════════════════════════════════════════════════════════
//
//  1. EDITING MODE ALWAYS ON
//     tableView.isEditing = true enables drag handles without delete buttons.
//     editingStyleForRowAt returns .none to hide delete controls.
//
//  2. HEADER SIZING
//     Table header (trip summary card) uses dynamic sizing. The
//     viewDidLayoutSubviews dance prevents infinite layout loops by tracking
//     lastHeaderHeight and using isAdjustingHeader guard.
//
//  3. TRAVEL DAY ASSOCIATION
//     Travel is conceptually "the travel that arrives on day N". Visually it
//     appears BEFORE day N's header. When finding which day a travel belongs
//     to, we look FORWARD to find the next dayHeader.
//
//  4. CONTEXT MENUS
//     Long-press on custom items shows Edit/Delete menu via UIKit context menus.
//     More reliable than SwiftUI's contextMenu in table cells.
//
//  ═══════════════════════════════════════════════════════════════════════════════
//  LIMITATIONS & KNOWN ISSUES
//  ═══════════════════════════════════════════════════════════════════════════════
//
//  1. Games are not individually reorderable - they're bundled as one row per day.
//     This is intentional: game times are fixed, reordering would be confusing.
//
//  2. Travel segments can't be deleted - they're structural to the trip route.
//     User can only move them within valid day ranges.
//
//  3. sortOrder precision: After millions of midpoint insertions, precision
//     could theoretically degrade. In practice, this never happens. If needed,
//     a "normalize sort orders" function could reassign 1.0, 2.0, 3.0...
//
//  4. SwiftUI views in cells: UIHostingConfiguration works well but has slight
//     overhead. For very long trips (100+ days), consider cell recycling optimization.
//
//  ═══════════════════════════════════════════════════════════════════════════════
//  RELATED FILES
//  ═══════════════════════════════════════════════════════════════════════════════
//
//  - ItineraryTableViewWrapper.swift: SwiftUI bridge, data transformation
//  - TripDetailView.swift: Parent view, owns state, handles callbacks
//  - ItineraryItem.swift: Domain model with (day, sortOrder, kind) positioning
//  - ItineraryItemService.swift: CloudKit persistence for itinerary items
//
//  ═══════════════════════════════════════════════════════════════════════════════

import UIKit
import SwiftUI

// MARK: - Data Models

/// Intermediate structure passed from ItineraryTableViewWrapper.
/// Contains one day's worth of data before flattening into rows.
///
/// Note: `items` contains only custom items (add buttons are generated during flattening).
/// `travelBefore` is the travel segment that ARRIVES on this day (visually before the header).
struct ItineraryDayData: Identifiable {
    let id: Int  // dayNumber (1-indexed)
    let dayNumber: Int
    let date: Date
    let games: [RichGame]
    var items: [ItineraryRowItem]  // Custom items for this day (sorted by sortOrder)
    var travelBefore: TravelSegment?  // Travel arriving this day (renders BEFORE day header)
    
    var isRestDay: Bool { games.isEmpty }
}

/// Represents a single row in the flattened table view.
///
/// The enum cases map 1:1 to visual row types. Each has a stable `id` for diffing
/// and an `isReorderable` flag controlling whether the drag handle appears.
///
/// ID format examples:
/// - dayHeader: "day:3"
/// - games: "games:3"
/// - travel: "travel:0:detroit->milwaukee" (index:lowercase, stable across sessions)
/// - customItem: "item:550e8400-e29b-41d4-a716-446655440000"
/// - addButton: "add:3"
enum ItineraryRowItem: Identifiable, Equatable {
    case dayHeader(dayNumber: Int, date: Date)  // Fixed: structural anchor (includes Add button)
    case games([RichGame], dayNumber: Int)      // Fixed: games are trip-determined
    case travel(TravelSegment, dayNumber: Int)  // Reorderable: within valid range
    case customItem(ItineraryItem)              // Reorderable: anywhere

    /// Stable identifier for table view diffing and external references.
    /// Travel IDs include segment index and are lowercase for consistency.
    var id: String {
        switch self {
        case .dayHeader(let dayNumber, _):
            return "day:\(dayNumber)"
        case .games(_, let dayNumber):
            return "games:\(dayNumber)"
        case .travel(let segment, let dayNumber):
            // Use segment UUID for unique diffing (segment index is not available here)
            return "travel:\(segment.id.uuidString):\(dayNumber)"
        case .customItem(let item):
            return "item:\(item.id.uuidString)"
        }
    }
    
    /// Controls whether UITableView shows the drag reorder handle.
    /// Day headers and games are structural - users can't move them.
    var isReorderable: Bool {
        switch self {
        case .dayHeader, .games:
            return false
        case .travel, .customItem:
            return true
        }
    }
    
    static func == (lhs: ItineraryRowItem, rhs: ItineraryRowItem) -> Bool {
        lhs.id == rhs.id
    }
}

// MARK: - Table View Controller

final class ItineraryTableViewController: UITableViewController {
    
    // MARK: - Properties
    
    private var flatItems: [ItineraryRowItem] = []
    var travelValidRanges: [String: ClosedRange<Int>] = [:]  // travelId -> valid day range
    var colorScheme: ColorScheme = .dark
    
    // Callbacks
    var onTravelMoved: ((String, Int, Double) -> Void)?  // travelId, newDay
    var onCustomItemMoved: ((UUID, Int, Double) -> Void)?  // itemId, newDay, newSortOrder
    var onCustomItemTapped: ((ItineraryItem) -> Void)?
    var onCustomItemDeleted: ((ItineraryItem) -> Void)?
    var onAddButtonTapped: ((Int) -> Void)?  // Just day number
    
    // Cell reuse identifiers
    private let dayHeaderCellId = "DayHeaderCell"
    private let gamesCellId = "GamesCell"
    private let travelCellId = "TravelCell"
    private let customItemCellId = "CustomItemCell"
    
    // Header sizing state - prevents infinite layout loops
    private var lastHeaderHeight: CGFloat = 0
    private var isAdjustingHeader = false
    
    // MARK: - Constraint-Aware Drag State
    //
    // These properties track the current drag operation for constraint validation
    // and visual feedback. They're populated when drag starts and cleared when it ends.
    
    /// The constraint system for validating item positions
    private var constraints: ItineraryConstraints?
    
    /// All itinerary items (needed to build constraints)
    private var allItineraryItems: [ItineraryItem] = []
    
    /// Trip day count for constraints
    private var tripDayCount: Int = 0
    
    /// The item currently being dragged (nil when no drag active)
    private var draggingItem: ItineraryRowItem?
    
    /// The day number the user is targeting during custom item drag (for stable positioning)
    private var dragTargetDay: Int?
    
    /// Row indices that are invalid drop targets for the current drag (for visual dimming)
    private var invalidRowIndices: Set<Int> = []
    
    /// Row indices that ARE valid drop targets - pre-calculated at drag start for stability
    /// Using a sorted array enables O(log n) nearest-neighbor lookup
    private var validDropRows: [Int] = []
    
    /// Valid destination rows in *proposed* coordinate space (after removing the source row).
    /// Precomputed at drag start by simulating the move and validating semantic constraints.
    private var validDestinationRowsProposed: [Int] = []
    
    
    /// IDs of games that act as barriers for the current travel drag (for gold highlighting)
    private var barrierGameIds: Set<String> = []
    
    /// Track whether we're currently in a valid drop zone (for haptic feedback)
    private var isInValidZone: Bool = true
    
    /// Haptic feedback generator for drag interactions
    private let feedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
    
    // MARK: - Lifecycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
    }
    
    /// Configures the UITableView for our specific use case.
    ///
    /// Key decisions:
    /// - `isEditing = true`: Shows drag handles on reorderable rows. We keep this permanently on
    ///   since the primary interaction IS reordering. Non-reorderable rows return false from
    ///   `canMoveRowAt` so they don't show handles.
    /// - `allowsSelectionDuringEditing = true`: Normally, editing mode disables row selection.
    ///   We need selection for tapping Add buttons and custom items, so we override this.
    /// - `separatorStyle = .none`: We use custom card styling per row type, so system separators
    ///   would look out of place.
    /// - `estimatedRowHeight = 80`: Helps UITableView's internal calculations. The actual height
    ///   is determined by UIHostingConfiguration's automatic sizing.
    /// - `contentInset.bottom = 40`: Adds padding at the bottom so the last row isn't flush
    ///   against the screen edge when scrolled to the end.
    private func setupTableView() {
        tableView.backgroundColor = .clear
        tableView.separatorStyle = .none
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 80
        tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 40, right: 0)
        
        // IMPORTANT: isEditing must be true for drag handles to appear.
        // This is different from "delete mode" editing - we disable delete controls
        // via editingStyleForRowAt returning .none.
        tableView.isEditing = true
        tableView.allowsSelectionDuringEditing = true
        
        // We use generic UITableViewCell with UIHostingConfiguration for content.
        // This gives us SwiftUI views with UIKit drag-and-drop. Different cell IDs
        // help with debugging but aren't strictly necessary since we fully replace
        // contentConfiguration each time.
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: dayHeaderCellId)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: gamesCellId)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: travelCellId)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: customItemCellId)
    }
    
    /// Installs a SwiftUI view as the table's header (appears above all rows).
    ///
    /// We wrap the view in a container because UITableView header sizing is notoriously
    /// tricky. The container provides a consistent Auto Layout context. The actual sizing
    /// is handled in `viewDidLayoutSubviews`.
    ///
    /// - Parameter view: A UIView (typically from UIHostingController wrapping SwiftUI content)
    func setTableHeader(_ view: UIView) {
        let containerView = UIView()
        containerView.backgroundColor = .clear
        containerView.addSubview(view)
        
        // Pin the content view to all edges of the container
        view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            view.topAnchor.constraint(equalTo: containerView.topAnchor),
            view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
        ])
        
        tableView.tableHeaderView = containerView
    }
    
    /// Handles dynamic header sizing.
    ///
    /// UITableView doesn't automatically size tableHeaderView - we must manually calculate
    /// the appropriate height and update the frame. This is called on every layout pass.
    ///
    /// **CRITICAL: Infinite Loop Prevention**
    /// Setting `tableView.tableHeaderView = headerView` triggers another layout pass.
    /// Without guards, this creates an infinite loop:
    ///   layoutSubviews → set header → layoutSubviews → set header → ...
    ///
    /// We prevent this with two mechanisms:
    /// 1. `isAdjustingHeader` flag blocks re-entry during our update
    /// 2. `lastHeaderHeight` skips updates when height hasn't meaningfully changed
    ///
    /// The 1-point threshold handles floating-point jitter that can occur between passes.
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        // Guard 1: Don't re-enter while we're in the middle of adjusting
        guard !isAdjustingHeader else { return }
        guard let headerView = tableView.tableHeaderView else { return }
        guard tableView.bounds.width > 0 else { return }
        
        // Calculate the ideal header size given the current table width
        let targetSize = CGSize(width: tableView.bounds.width, height: UIView.layoutFittingCompressedSize.height)
        let size = headerView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
        
        // Guard 2: Only update if height changed meaningfully (>1pt)
        // This prevents jitter from sub-point differences between passes
        let heightDelta = abs(headerView.frame.size.height - size.height)
        if heightDelta > 1 && abs(size.height - lastHeaderHeight) > 1 {
            isAdjustingHeader = true
            lastHeaderHeight = size.height
            headerView.frame.size.height = size.height
            // Re-assigning tableHeaderView forces UITableView to recalculate layout
            tableView.tableHeaderView = headerView
            isAdjustingHeader = false
        }
    }
    
    /// Transforms hierarchical day data into a flat row list and refreshes the table.
    ///
    /// Delegates to `ItineraryReorderingLogic.flattenDays` for the pure transformation,
    /// then updates the table view.
    ///
    /// - Parameters:
    ///   - days: Array of ItineraryDayData from ItineraryTableViewWrapper
    ///   - travelValidRanges: Dictionary mapping travel IDs to their valid day ranges
    ///   - itineraryItems: All ItineraryItem models for building constraints
    func reloadData(
        days: [ItineraryDayData],
        travelValidRanges: [String: ClosedRange<Int>],
        itineraryItems: [ItineraryItem] = []
    ) {
        self.travelValidRanges = travelValidRanges
        self.allItineraryItems = itineraryItems
        self.tripDayCount = days.count

        // Rebuild constraints with new data
        self.constraints = ItineraryConstraints(tripDayCount: tripDayCount, items: itineraryItems)

        // Use pure function for flattening
        flatItems = ItineraryReorderingLogic.flattenDays(days) { [weak self] segment in
            self?.findItineraryItem(for: segment)?.sortOrder
        }

        tableView.reloadData()
    }
    
    // MARK: - Row-to-Day Mapping Helpers (delegating to pure functions)

    /// Finds which day a row at the given index belongs to.
    private func dayNumber(forRow row: Int) -> Int {
        ItineraryReorderingLogic.dayNumber(in: flatItems, forRow: row)
    }

    /// Finds the row index of the day header for a specific day number.
    private func dayHeaderRow(forDay day: Int) -> Int? {
        ItineraryReorderingLogic.dayHeaderRow(in: flatItems, forDay: day)
    }

    /// Finds the row index of the travel segment arriving on a specific day.
    private func travelRow(forDay day: Int) -> Int? {
        ItineraryReorderingLogic.travelRow(in: flatItems, forDay: day)
    }
    
    // MARK: - Drag State Management
    //
    // These methods handle the start, update, and end of drag operations,
    // managing visual feedback (dimming invalid zones, highlighting barrier games)
    // and haptic feedback.
    
    /// Called when a drag operation begins.
    ///
    /// Performs the following setup:
    /// 1. Stores the dragging item
    /// 2. Calculates invalid row indices based on constraints
    /// 3. Identifies barrier games for visual highlighting (travel items only)
    /// 4. Triggers pickup haptic feedback
    /// 5. Applies visual dimming to invalid zones
    private func beginDrag(at indexPath: IndexPath) {
        let item = flatItems[indexPath.row]
        draggingItem = item
        isInValidZone = true
        
        // Calculate invalid zones and barriers based on item type
        switch item {
        case .travel(let segment, _):
            calculateTravelDragZones(segment: segment)
        case .customItem(let itineraryItem):
            calculateCustomItemDragZones(item: itineraryItem)
        default:
            // Day headers and games shouldn't be dragged
            invalidRowIndices = []
            barrierGameIds = []
        }
        
        // Trigger pickup haptic
        feedbackGenerator.prepare()
        feedbackGenerator.impactOccurred(intensity: 0.7)
        
        // Apply visual feedback
        applyDragVisualFeedback()
    }
    
    /// Called when a drag operation ends (item dropped).
    ///
    /// Clears all drag state and removes visual feedback:
    /// 1. Clears dragging item reference
    /// 2. Clears invalid row indices and barrier game IDs
    /// 3. Triggers drop haptic feedback
    /// 4. Removes visual dimming and highlighting
    private func endDrag() {
        draggingItem = nil
        dragTargetDay = nil
        invalidRowIndices = []
        validDropRows = []
        validDestinationRowsProposed = []
        barrierGameIds = []
        isInValidZone = true
        
        // Trigger drop haptic
        feedbackGenerator.impactOccurred(intensity: 0.5)
        
        // Remove visual feedback
        removeDragVisualFeedback()
    }
    
    /// Calculates invalid zones for a travel segment drag.
    /// Delegates to pure function and applies results to instance state.
    private func calculateTravelDragZones(segment: TravelSegment) {
        let zones = ItineraryReorderingLogic.calculateTravelDragZones(
            segment: segment,
            flatItems: flatItems,
            travelValidRanges: travelValidRanges,
            constraints: constraints,
            findTravelItem: { [weak self] segment in self?.findItineraryItem(for: segment) }
        )
        invalidRowIndices = zones.invalidRowIndices
        validDropRows = zones.validDropRows
        barrierGameIds = zones.barrierGameIds
    }

    /// Calculates invalid zones for a custom item drag.
    /// Delegates to pure function and applies results to instance state.
    private func calculateCustomItemDragZones(item: ItineraryItem) {
        let zones = ItineraryReorderingLogic.calculateCustomItemDragZones(item: item, flatItems: flatItems)
        invalidRowIndices = zones.invalidRowIndices
        validDropRows = zones.validDropRows
        barrierGameIds = zones.barrierGameIds
    }
    
    /// Finds the ItineraryItem model for a travel segment.
    ///
    /// Searches through allItineraryItems to find a matching travel item.
    /// Prefers matching by segmentIndex for disambiguation of repeat city pairs.
    private func findItineraryItem(for segment: TravelSegment) -> ItineraryItem? {
        let fromLower = segment.fromLocation.name.lowercased()
        let toLower = segment.toLocation.name.lowercased()

        // Find all matching travel items by city pair
        let matches = allItineraryItems.filter { item in
            guard case .travel(let info) = item.kind else { return false }
            return info.fromCity.lowercased() == fromLower
            && info.toCity.lowercased() == toLower
        }

        // If only one match, return it
        if matches.count <= 1 { return matches.first }

        // Multiple matches (repeat city pair) — try to match by segment UUID identity
        // The segment.id is a UUID that identifies the specific TravelSegment instance
        // We match through the allItineraryItems which have segmentIndex set
        return matches.first ?? nil
    }
    
    /// Applies visual feedback during drag.
    ///
    /// - Invalid zones: Dimmed with alpha 0.3
    /// - Barrier games: Highlighted with gold border
    private func applyDragVisualFeedback() {
        for (index, _) in flatItems.enumerated() {
            guard let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) else { continue }
            
            if invalidRowIndices.contains(index) {
                // Dim invalid rows
                UIView.animate(withDuration: 0.2) {
                    cell.contentView.alpha = 0.3
                }
            }
            
            // Check if this row contains a barrier game
            if case .games(let games, _) = flatItems[index] {
                let gameIds = games.map { $0.game.id }
                let hasBarrier = gameIds.contains { barrierGameIds.contains($0) }
                if hasBarrier {
                    // Apply gold border to barrier game cells
                    UIView.animate(withDuration: 0.2) {
                        cell.contentView.layer.borderWidth = 2
                        cell.contentView.layer.borderColor = UIColor.systemYellow.cgColor
                        cell.contentView.layer.cornerRadius = 12
                    }
                }
            }
        }
    }
    
    /// Removes visual feedback after drag ends.
    private func removeDragVisualFeedback() {
        for (index, _) in flatItems.enumerated() {
            guard let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) else { continue }
            
            UIView.animate(withDuration: 0.2) {
                cell.contentView.alpha = 1.0
                cell.contentView.layer.borderWidth = 0
                cell.contentView.layer.borderColor = nil
            }
        }
    }
    
    /// Called during drag to check if the hover position changed validity.
    ///
    /// Triggers haptic feedback when transitioning between valid/invalid zones.
    private func checkZoneTransition(at proposedRow: Int) {
        let isValid = !invalidRowIndices.contains(proposedRow)
        
        if isValid != isInValidZone {
            isInValidZone = isValid
            if isValid {
                // Entering valid zone - soft haptic
                let lightGenerator = UIImpactFeedbackGenerator(style: .light)
                lightGenerator.impactOccurred()
            } else {
                // Entering invalid zone - error haptic
                let errorGenerator = UINotificationFeedbackGenerator()
                errorGenerator.notificationOccurred(.warning)
            }
        }
    }
    
    // MARK: - UITableViewDataSource
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        // We use a single section with a flat list. This simplifies reordering logic
        // since we don't need to handle cross-section moves.
        return 1
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return flatItems.count
    }
    
    /// Configures and returns the cell for a row.
    ///
    /// Each row type dispatches to a specific configure method. We use UIHostingConfiguration
    /// to embed SwiftUI views in UITableViewCells, getting the best of both worlds:
    /// - UIKit's mature drag-and-drop with real-time feedback
    /// - SwiftUI's declarative, easy-to-style views
    ///
    /// Note: We completely replace the cell's contentConfiguration each time. This means
    /// cell reuse is less important than in traditional UIKit - the SwiftUI view is
    /// recreated regardless. This is fine for our use case (typically <50 rows).
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let item = flatItems[indexPath.row]
        
        switch item {
        case .dayHeader(let dayNumber, let date):
            let cell = tableView.dequeueReusableCell(withIdentifier: dayHeaderCellId, for: indexPath)
            configureDayHeaderCell(cell, dayNumber: dayNumber, date: date)
            return cell
            
        case .games(let games, _):
            let cell = tableView.dequeueReusableCell(withIdentifier: gamesCellId, for: indexPath)
            configureGamesCell(cell, games: games)
            return cell
            
        case .travel(let segment, _):
            let cell = tableView.dequeueReusableCell(withIdentifier: travelCellId, for: indexPath)
            configureTravelCell(cell, segment: segment)
            return cell
            
        case .customItem(let customItem):
            let cell = tableView.dequeueReusableCell(withIdentifier: customItemCellId, for: indexPath)
            configureCustomItemCell(cell, item: customItem)
            return cell
        }
    }
    
    // MARK: - Reordering (Drag and Drop)
    
    /// Controls which rows show the drag reorder handle.
    ///
    /// Returns `isReorderable` from the row item:
    /// - `true` for travel segments and custom items (user can move these)
    /// - `false` for day headers and games (structural items)
    override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return flatItems[indexPath.row].isReorderable
    }
    
    /// Called AFTER the user lifts their finger to complete a drag.
    ///
    /// At this point, UITableView has already visually moved the row. Our job is to:
    /// 1. Update our data model (`flatItems`) to reflect the new position
    /// 2. Notify the parent view via callbacks so it can persist the change
    /// 3. Clear drag state and remove visual feedback
    ///
    /// **For travel segments:** We notify `onTravelMoved` with the travel ID and new day.
    /// The parent stores this in `travelDayOverrides` (not persisted to CloudKit).
    ///
    /// **For custom items:** We notify `onCustomItemMoved` with the item ID, new day,
    /// and calculated sortOrder. The parent updates the item and syncs to CloudKit.
    override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let item = flatItems[sourceIndexPath.row]
        
        // End drag state and remove visual feedback
        endDrag()
        
        // Update our in-memory data model
        flatItems.remove(at: sourceIndexPath.row)
        flatItems.insert(item, at: destinationIndexPath.row)
        
        // Notify parent view of the change
        switch item {
        case .travel(let segment, _):
            // Travel is positioned within a day using sortOrder (can be before/after games)
            let destinationDay = dayNumber(forRow: destinationIndexPath.row)
            let sortOrder = calculateSortOrder(at: destinationIndexPath.row)
            let segIdx = findItineraryItem(for: segment)?.travelInfo?.segmentIndex ?? 0
            let travelId = "travel:\(segIdx):\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
            onTravelMoved?(travelId, destinationDay, sortOrder)
            
        case .customItem(let customItem):
            // Calculate the new day and sortOrder for the dropped position
            let destinationDay = dayNumber(forRow: destinationIndexPath.row)
            let sortOrder = calculateSortOrder(at: destinationIndexPath.row)
            
            // DEBUG: Log the final state after insertion
            print("🎯 [Drop] source=\(sourceIndexPath.row) → dest=\(destinationIndexPath.row)")
            print("🎯 [Drop] flatItems around dest:")
            for i in max(0, destinationIndexPath.row - 2)...min(flatItems.count - 1, destinationIndexPath.row + 2) {
                let marker = i == destinationIndexPath.row ? "→" : " "
                print("🎯 [Drop] \(marker) [\(i)] \(flatItems[i])")
            }
            print("🎯 [Drop] Calculated day=\(destinationDay), sortOrder=\(sortOrder)")
            
            onCustomItemMoved?(customItem.id, destinationDay, sortOrder)
            
        default:
            break  // Day headers, games, add buttons can't be moved
        }
    }
    
    /// Determines which day a travel segment belongs to at a given row position.
    private func dayForTravelAt(row: Int) -> Int {
        ItineraryReorderingLogic.dayForTravelAt(row: row, in: flatItems)
    }
    
    /// Called DURING a drag to validate and potentially modify the drop position.
    ///
    /// Delegates constraint logic to pure functions, handles only UIKit-specific concerns:
    /// - Drag state initialization (first call)
    /// - Haptic/visual feedback
    /// - Converting pure function results to IndexPath
    ///
    /// - Parameters:
    ///   - sourceIndexPath: Where the item is being dragged FROM
    ///   - proposedDestinationIndexPath: Where the user is trying to drop
    /// - Returns: The actual destination (may differ from proposed)
    override func tableView(
        _ tableView: UITableView,
        targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath,
        toProposedIndexPath proposedDestinationIndexPath: IndexPath
    ) -> IndexPath {

        let sourceRow = sourceIndexPath.row
        let item = flatItems[sourceRow]

        // Drag start detection - initialize state and compute valid destinations
        if draggingItem == nil {
            beginDrag(at: sourceIndexPath)
            validDestinationRowsProposed = computeValidDestinationRowsProposed(sourceRow: sourceRow, dragged: item)
        }

        // Clamp proposed row
        var proposedRow = proposedDestinationIndexPath.row
        proposedRow = min(max(1, proposedRow), max(0, flatItems.count - 1))

        // Haptics / visuals
        checkZoneTransition(at: proposedRow)

        // Use pure function for target calculation
        let targetRow = ItineraryReorderingLogic.calculateTargetRow(
            proposedRow: proposedRow,
            validDestinationRows: validDestinationRowsProposed,
            sourceRow: sourceRow
        )
        return IndexPath(row: targetRow, section: 0)
    }
    
    // MARK: - Drag Destination Precomputation (delegating to pure functions)

    /// Computes all valid destination rows in **proposed** coordinate space.
    /// Delegates to pure function with closures for model lookups.
    private func computeValidDestinationRowsProposed(sourceRow: Int, dragged: ItineraryRowItem) -> [Int] {
        ItineraryReorderingLogic.computeValidDestinationRowsProposed(
            flatItems: flatItems,
            sourceRow: sourceRow,
            dragged: dragged,
            travelValidRanges: travelValidRanges,
            constraints: constraints,
            findTravelItem: { [weak self] segment in self?.findItineraryItem(for: segment) },
            makeTravelItem: { [weak self] segment in
                ItineraryItem(
                    tripId: self?.allItineraryItems.first?.tripId ?? UUID(),
                    day: 1,
                    sortOrder: 0,
                    kind: .travel(TravelInfo(
                        fromCity: segment.fromLocation.name,
                        toCity: segment.toLocation.name,
                        distanceMeters: segment.distanceMeters,
                        durationSeconds: segment.durationSeconds
                    ))
                )
            },
            findTravelSortOrder: { [weak self] segment in self?.findItineraryItem(for: segment)?.sortOrder }
        )
    }
    
    // MARK: - Editing Style Configuration
    
    /// Returns `.none` to hide the red delete circle that normally appears in editing mode.
    ///
    /// We have `tableView.isEditing = true` to enable drag handles, but we don't want
    /// the standard "swipe to delete" UI. Returning `.none` hides delete controls.
    /// (Deletion is handled via context menu instead.)
    override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
        return .none
    }
    
    /// Returns `false` to prevent cells from indenting when in editing mode.
    ///
    /// By default, editing mode indents cells to make room for delete controls.
    /// Since we're not using those controls, we disable indentation to keep
    /// our custom card layouts flush with the edges.
    override func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
        return false
    }
    
    // MARK: - Row Selection
    
    /// Handles taps on rows.
    ///
    /// We only respond to taps on:
    /// - **Custom items:** Opens edit sheet via `onCustomItemTapped`
    ///
    /// Day headers (with embedded Add button), games, and travel don't respond to row taps.
    /// The Add button within the day header handles its own tap via SwiftUI Button.
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // Immediately deselect to remove highlight
        tableView.deselectRow(at: indexPath, animated: true)
        
        let item = flatItems[indexPath.row]
        
        switch item {
        case .customItem(let customItem):
            onCustomItemTapped?(customItem)
            
        default:
            break  // Other row types don't respond to row taps
        }
    }
    
    // MARK: - Context Menu (Long Press)
    
    /// Provides context menu for long-press on custom items.
    ///
    /// Only custom items have a context menu (Edit/Delete). Other row types
    /// return nil, meaning no menu appears on long-press.
    ///
    /// We use UIKit context menus rather than SwiftUI's `.contextMenu()` because
    /// they're more reliable inside UITableViewCells and provide better preview behavior.
    override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
        let item = flatItems[indexPath.row]
        
        // Only custom items have context menus
        guard case .customItem(let customItem) = item else {
            return nil
        }
        
        return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
            let editAction = UIAction(title: "Edit", image: UIImage(systemName: "pencil")) { [weak self] _ in
                self?.onCustomItemTapped?(customItem)
            }
            
            let deleteAction = UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { [weak self] _ in
                self?.onCustomItemDeleted?(customItem)
            }
            
            return UIMenu(title: "", children: [editAction, deleteAction])
        }
    }
    
    // MARK: - Sort Order Calculation

    /// Calculates the sortOrder for an item dropped at the given row position.
    /// Delegates to pure function with closure for travel sortOrder lookup.
    private func calculateSortOrder(at row: Int) -> Double {
        ItineraryReorderingLogic.calculateSortOrder(
            in: flatItems,
            at: row,
            findTravelSortOrder: { [weak self] segment in self?.findItineraryItem(for: segment)?.sortOrder }
        )
    }
    
    // MARK: - Cell Configuration
    //
    // Each configure method sets up a UITableViewCell with:
    // 1. UIHostingConfiguration wrapping a SwiftUI view
    // 2. .margins(.all, 0) to remove default content margins
    // 3. .background(.clear) to let parent background show through
    // 4. cell.backgroundColor = .clear for the same reason
    // 5. cell.selectionStyle based on whether the row is tappable
    
    /// Day header cell - shows "Day N · Date" text with embedded Add button.
    /// The Add button handles its own tap via SwiftUI Button (not row selection).
    private func configureDayHeaderCell(_ cell: UITableViewCell, dayNumber: Int, date: Date) {
        cell.contentConfiguration = UIHostingConfiguration {
            DaySectionHeaderView(
                dayNumber: dayNumber,
                date: date,
                colorScheme: colorScheme,
                onAddTapped: { [weak self] in
                    self?.onAddButtonTapped?(dayNumber)
                }
            )
        }
        .margins(.all, 0)
        .background(.clear)
        
        cell.backgroundColor = .clear
        cell.selectionStyle = .none  // Row itself isn't selectable; Add button handles taps
    }
    
    /// Games cell - shows city label and game cards for a day.
    /// Not selectable (games are informational, not editable).
    private func configureGamesCell(_ cell: UITableViewCell, games: [RichGame]) {
        cell.contentConfiguration = UIHostingConfiguration {
            GamesRowView(games: games, colorScheme: colorScheme)
        }
        .margins(.all, 0)
        .background(.clear)
        
        cell.backgroundColor = .clear
        cell.selectionStyle = .none
    }
    
    /// Travel cell - shows route card (from → to with distance/duration).
    /// Not selectable but IS draggable (handled by canMoveRowAt).
    private func configureTravelCell(_ cell: UITableViewCell, segment: TravelSegment) {
        cell.contentConfiguration = UIHostingConfiguration {
            TravelRowView(segment: segment, colorScheme: colorScheme)
        }
        .margins(.all, 0)
        .background(.clear)
        
        cell.backgroundColor = .clear
        cell.selectionStyle = .none
    }
    
    /// Custom item cell - shows user-added item with category icon.
    /// Selectable (opens edit sheet on tap) and draggable.
    private func configureCustomItemCell(_ cell: UITableViewCell, item: ItineraryItem) {
        cell.contentConfiguration = UIHostingConfiguration {
            CustomItemRowView(item: item, colorScheme: colorScheme)
        }
        .margins(.all, 0)
        .background(.clear)
        
        cell.backgroundColor = .clear
        cell.selectionStyle = .default  // Shows highlight on tap
    }
}

// MARK: - SwiftUI Row Views
//
// These views are embedded in UITableViewCells via UIHostingConfiguration.
// They're simple, stateless views - all state management happens in the parent.
//
// Design notes:
// - Use Theme colors for consistency with the rest of the app
// - Pass colorScheme explicitly (UIHostingConfiguration doesn't inherit environment)
// - Padding is handled by the views themselves (cell margins are set to 0)

/// Day header row - displays "Day N · Date" with embedded Add button.
///
/// The Add button is part of the header row to prevent items from being
/// dragged between the header and the Add button. This ensures "Day N" and
/// "+ Add" always stay together as an atomic unit.
///
/// Layout:
/// ```
/// Day 3 · Sunday, Mar 8                    + Add
/// ```
struct DaySectionHeaderView: View {
    let dayNumber: Int
    let date: Date
    let colorScheme: ColorScheme
    let onAddTapped: () -> Void
    
    private var formattedDate: String {
        let formatter = DateFormatter()
        formatter.dateFormat = "EEEE, MMM d"  // "Sunday, Mar 8"
        return formatter.string(from: date)
    }
    
    var body: some View {
        HStack(alignment: .firstTextBaseline) {
            // Day label and date
            Text("Day \(dayNumber)")
                .font(.title3)
                .fontWeight(.bold)
                .foregroundStyle(Theme.textPrimary(colorScheme))
            
            Text("·")
                .foregroundStyle(Theme.textMuted(colorScheme))
            
            Text(formattedDate)
                .font(.subheadline)
                .foregroundStyle(Theme.textSecondary(colorScheme))
            
            Spacer()
            
            // Add button (right-aligned, bordered capsule for discoverability)
            Button(action: onAddTapped) {
                Label("Add", systemImage: "plus")
                    .font(.subheadline.weight(.medium))
            }
            .buttonStyle(.bordered)
            .buttonBorderShape(.capsule)
            .tint(Theme.warmOrange)
            .accessibilityLabel("Add item to Day \(dayNumber)")
            .accessibilityHint("Add restaurants, activities, or notes to this day")
        }
        .padding(.horizontal, Theme.Spacing.lg)
        .padding(.top, Theme.Spacing.lg)  // More space above for section separation
        .padding(.bottom, Theme.Spacing.sm)
    }
}

/// Games row - displays all games for a day with city label.
///
/// Games are bundled into a single row (not individually reorderable) because:
/// 1. Game times are fixed by schedules - reordering would be meaningless
/// 2. Users don't control which games are in the trip - that's the planning engine
/// 3. Keeping them together reinforces "these are your games for this day"
///
/// The city label appears above the game cards (e.g., "Milwaukee" above the Bucks game).
struct GamesRowView: View {
    let games: [RichGame]
    let colorScheme: ColorScheme
    
    var body: some View {
        VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
            // City label (derived from first game's stadium)
            // All games on same day are typically in the same city
            if let firstGame = games.first {
                Text(firstGame.stadium.city)
                    .font(.subheadline)
                    .foregroundStyle(Theme.warmOrange)
                    .padding(.horizontal, Theme.Spacing.lg)
            }
            
            // Individual game cards
            ForEach(games, id: \.game.id) { richGame in
                GameRowCompact(richGame: richGame, colorScheme: colorScheme)
            }
        }
        .padding(.horizontal, Theme.Spacing.lg)
        .padding(.bottom, Theme.Spacing.xs)
    }
}

/// Individual game card within GamesRowView.
///
/// Shows sport-colored accent bar, sport badge, matchup, stadium, and time.
/// Uses elevated card background to distinguish from surrounding content.
struct GameRowCompact: View {
    let richGame: RichGame
    let colorScheme: ColorScheme
    
    private var formattedTime: String {
        richGame.localGameTimeShort  // Stadium local time
    }
    
    var body: some View {
        HStack(spacing: Theme.Spacing.md) {
            // Left accent bar in sport's color (NBA=orange, MLB=blue, etc.)
            RoundedRectangle(cornerRadius: 3)
                .fill(richGame.game.sport.color)
                .frame(width: 5)
            
            VStack(alignment: .leading, spacing: 6) {
                // Sport badge (e.g., "NBA")
                Text(richGame.game.sport.rawValue)
                    .font(.caption)
                    .fontWeight(.bold)
                    .foregroundStyle(richGame.game.sport.color)
                
                // Matchup (e.g., "ORL @ MIL")
                Text(richGame.matchupDescription)
                    .font(.body)
                    .fontWeight(.semibold)
                    .foregroundStyle(Theme.textPrimary(colorScheme))
                
                // Stadium name with icon
                HStack(spacing: 6) {
                    Image(systemName: "building.2")
                        .font(.caption)
                        .accessibilityHidden(true)
                    Text(richGame.stadium.name)
                        .font(.subheadline)
                }
                .foregroundStyle(Theme.textSecondary(colorScheme))
            }
            
            Spacer()

            // Game time (prominently displayed)
            Text(formattedTime)
                .font(.title3)
                .fontWeight(.medium)
                .foregroundStyle(Theme.warmOrange)

            // Map button to open stadium location
            Button {
                AppleMapsLauncher.openLocation(
                    coordinate: richGame.stadium.coordinate,
                    name: richGame.stadium.name
                )
            } label: {
                Image(systemName: "map")
                    .font(.body)
                    .foregroundStyle(Theme.warmOrange)
                    .padding(8)
                    .background(Theme.warmOrange.opacity(0.15))
                    .clipShape(Circle())
            }
            .buttonStyle(.plain)
            .accessibilityLabel("Open \(richGame.stadium.name) in Maps")
            .accessibilityHint("Opens this stadium location in Apple Maps")
        }
        .padding(Theme.Spacing.md)
        .background(Theme.cardBackgroundElevated(colorScheme))
        .clipShape(RoundedRectangle(cornerRadius: 12))
    }
}

/// Travel row - displays a travel segment (driving between cities).
///
/// Shows:
/// - Car icon in orange circle
/// - From city → To city vertically stacked
/// - Distance and duration between them
///
/// This row IS draggable (has drag handle) and can be moved within its valid
/// day range. Moving travel updates where it appears in the itinerary but
/// doesn't change the actual route.
struct TravelRowView: View {
    let segment: TravelSegment
    let colorScheme: ColorScheme
    
    var body: some View {
        HStack(spacing: Theme.Spacing.md) {
            // Car icon in subtle orange circle
            ZStack {
                Circle()
                    .fill(Theme.warmOrange.opacity(0.15))
                    .frame(width: 40, height: 40)
                
                Image(systemName: "car.fill")
                    .font(.body)
                    .foregroundStyle(Theme.warmOrange)
            }
            
            // Route details: From → (distance/duration) → To
            VStack(alignment: .leading, spacing: 4) {
                Text(segment.fromLocation.name)
                    .font(.subheadline)
                    .fontWeight(.medium)
                    .foregroundStyle(Theme.textPrimary(colorScheme))
                
                // Distance and duration on the connecting line
                HStack(spacing: 4) {
                    Image(systemName: "arrow.down")
                        .font(.caption2)
                        .foregroundStyle(Theme.textMuted(colorScheme))
                    
                    Text("\(segment.formattedDistance) · \(segment.formattedDuration)")
                        .font(.caption)
                        .foregroundStyle(Theme.textMuted(colorScheme))
                }
                
                Text(segment.toLocation.name)
                    .font(.subheadline)
                    .fontWeight(.medium)
                    .foregroundStyle(Theme.textPrimary(colorScheme))
            }

            Spacer()

            // Map button to open driving directions
            if let fromCoord = segment.fromLocation.coordinate,
               let toCoord = segment.toLocation.coordinate {
                Button {
                    AppleMapsLauncher.openDirections(
                        from: fromCoord,
                        fromName: segment.fromLocation.name,
                        to: toCoord,
                        toName: segment.toLocation.name
                    )
                } label: {
                    Image(systemName: "map")
                        .font(.body)
                        .foregroundStyle(Theme.warmOrange)
                        .padding(8)
                        .background(Theme.warmOrange.opacity(0.15))
                        .clipShape(Circle())
                }
                .buttonStyle(.plain)
                .accessibilityLabel("Get directions from \(segment.fromLocation.name) to \(segment.toLocation.name)")
                .accessibilityHint("Opens this stadium location in Apple Maps")
            }
        }
        .padding(Theme.Spacing.md)
        .background(Theme.cardBackground(colorScheme))
        .clipShape(RoundedRectangle(cornerRadius: 12))
        .padding(.horizontal, Theme.Spacing.lg)
        .padding(.vertical, Theme.Spacing.xs)
    }
}

/// Custom item row - displays a user-added itinerary item.
///
/// Shows:
/// - Category emoji icon (🍽️, 🎢, ⛽, etc.)
/// - Title with optional map pin indicator
/// - Address (if item has location)
/// - Chevron indicating tappable
///
/// Visual treatment uses a subtle orange tint to distinguish user-added items
/// from trip-determined content (games, travel).
///
/// This row is both tappable (opens edit sheet) and draggable.
struct CustomItemRowView: View {
    let item: ItineraryItem
    let colorScheme: ColorScheme
    
    private var customInfo: CustomInfo? {
        item.customInfo
    }
    
    var body: some View {
        HStack(spacing: Theme.Spacing.sm) {
            // Category icon (emoji)
            if let info = customInfo {
                Text(info.icon)
                    .font(.title3)
                
                VStack(alignment: .leading, spacing: 2) {
                    HStack(spacing: 4) {
                        Text(info.title)
                            .font(.subheadline)
                            .foregroundStyle(Theme.textPrimary(colorScheme))
                            .lineLimit(1)
                        
                        // Map pin indicator for items with coordinates
                        if info.isMappable {
                            Image(systemName: "mappin.circle.fill")
                                .font(.caption)
                                .foregroundStyle(Theme.warmOrange)
                                .accessibilityHidden(true)
                        }
                    }
                    
                    // Address subtitle (shown only if present)
                    if let address = info.address, !address.isEmpty {
                        Text(address)
                            .font(.caption)
                            .foregroundStyle(Theme.textMuted(colorScheme))
                            .lineLimit(1)
                    }
                }
            }
            
            Spacer()

            // Map button for items with GPS coordinates
            if let info = customInfo, let coordinate = info.coordinate {
                Button {
                    AppleMapsLauncher.openLocation(
                        coordinate: coordinate,
                        name: info.title
                    )
                } label: {
                    Image(systemName: "map")
                        .font(.subheadline)
                        .foregroundStyle(Theme.warmOrange)
                        .padding(6)
                        .background(Theme.warmOrange.opacity(0.15))
                        .clipShape(Circle())
                }
                .buttonStyle(.plain)
                .accessibilityLabel("Open \(info.title) in Maps")
                .accessibilityHint("Opens this stadium location in Apple Maps")
            }

            // Chevron indicates this is tappable
            Image(systemName: "chevron.right")
                .foregroundStyle(.tertiary)
                .font(.caption)
                .accessibilityHidden(true)
        }
        .padding(.horizontal, Theme.Spacing.md)
        .padding(.vertical, Theme.Spacing.sm)
        // Layered background: base card + orange tint
        .background {
            RoundedRectangle(cornerRadius: 8)
                .fill(Theme.cardBackground(colorScheme))
            RoundedRectangle(cornerRadius: 8)
                .fill(Theme.warmOrange.opacity(0.1))
        }
        // Subtle orange border for extra distinction
        .overlay {
            RoundedRectangle(cornerRadius: 8)
                .strokeBorder(Theme.warmOrange.opacity(0.3), lineWidth: 1)
        }
        .padding(.horizontal, Theme.Spacing.lg)
        .padding(.vertical, Theme.Spacing.xs)
    }
}
