# Itinerary Refactor Design

## Goal

Refactor the trip itinerary system to use simple sort-order positioning instead of anchor-based positioning, enabling movable travel segments with hard guardrails and custom items that can be moved anywhere.

## Problem Statement

The current anchor-based system (`anchorType`, `anchorId`, `anchorDay`) is overly complex and fragile:
- Anchor inference during drag/drop frequently fails
- Custom items "disappear" when anchors become invalid
- Route calculation doesn't match visual order
- Code is hard to reason about and debug

## Design Decisions

1. **Travel guardrails**: City-bound - travel can only be positioned between the last game in the departure city and the first game in the destination city
2. **Custom item routing**: Full position ordering - route follows exact visual position of items
3. **Data model**: Simple sort order - replace anchors with `day: Int` and `sortOrder: Double`

---

## Section 1: Data Model

### Current Model (Being Replaced)
```swift
struct CustomItineraryItem {
    var anchorType: AnchorType  // .startOfDay, .afterGame, .afterTravel
    var anchorId: String?       // game ID or travel ID
    var anchorDay: Int
    var sortOrder: Int
}
```

### New Model
```swift
struct CustomItineraryItem {
    var day: Int           // Which day (1-indexed)
    var sortOrder: Double  // Position within day (0.0, 1.0, 2.0... or 0.5 for between)
    // Remove: anchorType, anchorId
}
```

### Why Double for sortOrder?
- Insert between items without rewriting all sort orders
- Example: items at 1.0 and 2.0, insert at 1.5
- Periodically normalize (1, 2, 3...) during sync

### Travel Day Storage
```swift
// In Trip or separate storage
var travelDayOverrides: [String: Int]  // "milwaukee->salt lake": 4
```

### Migration
- Existing items: `day = anchorDay`, `sortOrder = Double(sortOrder)`
- One-time migration on app update
- CloudKit schema: add `day`, `sortOrderDouble`, deprecate anchor fields

---

## Section 2: Itinerary Building Logic

### Single Build Function
```swift
func buildItinerary() -> [ItineraryRow] {
    var rows: [ItineraryRow] = []

    for dayNum in 1...tripDays.count {
        let dayDate = tripDays[dayNum - 1]

        // 1. Travel arriving this day (if any)
        if let travel = travelArrivingOn(day: dayNum) {
            rows.append(.travel(travel, canMove: travelCanMove(travel)))
        }

        // 2. Day header
        let games = gamesOn(date: dayDate)
        rows.append(.dayHeader(dayNum, dayDate, games))

        // 3. Custom items for this day (sorted)
        let items = customItems
            .filter { $0.day == dayNum }
            .sorted { $0.sortOrder < $1.sortOrder }
        for item in items {
            rows.append(.customItem(item))
        }

        // 4. Add button
        rows.append(.addButton(dayNum))
    }

    return rows
}
```

### Travel Constraints
```swift
func travelValidRange(_ segment: TravelSegment) -> ClosedRange<Int> {
    let lastGameInFromCity = findLastGameDay(in: segment.fromCity)
    let firstGameInToCity = findFirstGameDay(in: segment.toCity)

    // Travel must be AFTER last game in departure city
    // Travel must be ON or BEFORE first game in destination city
    let minDay = lastGameInFromCity + 1
    let maxDay = firstGameInToCity

    return minDay...maxDay
}
```

### Route Waypoints
```swift
var routeWaypoints: [CLLocationCoordinate2D] {
    var waypoints: [CLLocationCoordinate2D] = []

    for row in buildItinerary() {
        switch row {
        case .dayHeader(_, _, let games):
            // Add game locations in order
            for game in games {
                waypoints.append(game.stadium.coordinate)
            }
        case .customItem(let item):
            // Add custom item if it has location
            if let coord = item.coordinate {
                waypoints.append(coord)
            }
        default:
            break
        }
    }

    return waypoints
}
```

---

## Section 3: Drag & Drop Handling

### Move Custom Item
```swift
func moveCustomItem(_ item: CustomItineraryItem, to targetDay: Int, afterRow: Int) {
    // 1. Determine new sortOrder based on position
    let itemsInTargetDay = customItems.filter { $0.day == targetDay }
    let newSortOrder = calculateSortOrder(afterRow: afterRow, existingItems: itemsInTargetDay)

    // 2. Update item directly - no anchor inference!
    var updated = item
    updated.day = targetDay
    updated.sortOrder = newSortOrder

    // 3. Save to CloudKit
    Task { try await CustomItemService.shared.updateItem(updated) }
}
```

### Move Travel
```swift
func moveTravel(_ segment: TravelSegment, to targetDay: Int) {
    let validRange = travelValidRange(segment)

    // Enforce guardrails
    guard validRange.contains(targetDay) else {
        showAlert("Travel must be between Day \(validRange.lowerBound) and Day \(validRange.upperBound)")
        return
    }

    // Store override
    let travelId = "\(segment.fromCity.lowercased())->\(segment.toCity.lowercased())"
    travelDayOverrides[travelId] = targetDay

    // Persist (CloudKit or local)
    saveTravelOverrides()
}
```

### Calculate Sort Order
```swift
func calculateSortOrder(afterRow: Int, existingItems: [CustomItineraryItem]) -> Double {
    // If inserting at start, use 0.5 before first item
    if afterRow == 0 {
        let firstOrder = existingItems.first?.sortOrder ?? 1.0
        return firstOrder - 1.0
    }

    // If inserting at end, use last + 1
    if afterRow >= existingItems.count {
        let lastOrder = existingItems.last?.sortOrder ?? 0.0
        return lastOrder + 1.0
    }

    // Insert between two items
    let before = existingItems[afterRow - 1].sortOrder
    let after = existingItems[afterRow].sortOrder
    return (before + after) / 2.0
}
```

---

## Section 4: UI Implementation

### Keep UITableView
The existing `ItineraryTableViewController` works well for drag/drop. Changes:

1. **Simplified data source**: Just iterate `buildItinerary()` result
2. **Move handling**: Direct day/sortOrder updates, no anchor inference
3. **Travel rows**: Show drag handle only when `canMove` is true

### Visual Feedback for Travel Constraints
```swift
// In tableView(_:targetIndexPathForMove...)
func tableView(_ tableView: UITableView,
               targetIndexPathForMoveFromRowAt source: IndexPath,
               toProposedIndexPath proposed: IndexPath) -> IndexPath {

    guard case .travel(let segment, _) = flatItems[source.row] else {
        return proposed  // Custom items can go anywhere
    }

    // For travel, clamp to valid day range
    let targetDay = dayNumber(for: proposed)
    let validRange = travelValidRange(segment)

    if targetDay < validRange.lowerBound {
        return indexPath(forDay: validRange.lowerBound, position: .start)
    }
    if targetDay > validRange.upperBound {
        return indexPath(forDay: validRange.upperBound, position: .start)
    }

    return proposed
}
```

### Wrapper Updates
`ItineraryTableViewWrapper` passes:
- `customItems: [CustomItineraryItem]`
- `travelDayOverrides: [String: Int]`
- `onCustomItemMoved: (UUID, Int, Double) -> Void`  // id, day, sortOrder
- `onTravelMoved: (String, Int) -> Void`  // travelId, day

---

## Files to Modify

| File | Changes |
|------|---------|
| `CustomItineraryItem.swift` | Replace anchors with `day` + `sortOrder: Double` |
| `CKCustomItineraryItem.swift` | Update CloudKit field mappings |
| `CustomItemService.swift` | Update CRUD for new fields |
| `ItineraryTableViewController.swift` | Simplify move logic, remove anchor inference |
| `ItineraryTableViewWrapper.swift` | Update callbacks, simplify data building |
| `TripDetailView.swift` | Update `routeWaypoints` to use build order |

## Files Unchanged
- `TravelSegment.swift` - No model changes needed
- `Trip.swift` - Travel overrides stored separately
- `CustomItemRow.swift` - Display unchanged

---

## Migration Plan

1. Add new fields to CloudKit schema (non-breaking)
2. App update reads old anchors, writes new fields
3. Backfill: `day = anchorDay`, `sortOrder = Double(existing sortOrder)`
4. After transition period, remove anchor fields from code

---

## Benefits

1. **Simpler mental model**: Position = (day, sortOrder), that's it
2. **No inference bugs**: What you drop is what you get
3. **Route matches visual**: Waypoints follow exact display order
4. **Easier debugging**: Just check day and sortOrder values
5. **Travel guardrails**: Clear constraints, enforced at drag time
