# Stadium Identity System

How SportsTime handles stadium renames, new stadiums, and team relocations while preserving user data.

## Architecture Overview

The system uses **immutable canonical IDs** for all references and **mutable display data** for names/locations. User data only stores canonical IDs, so it never needs migration when real-world changes occur.

```
User data → canonical IDs (stable) → current display names (via StadiumIdentityService)
```

## Data Models

### Canonical Data (Synced from CloudKit)

```swift
// Core identity - canonicalId NEVER changes
CanonicalStadium:
  canonicalId: "stadium_nba_los_angeles_lakers"  // Immutable
  name: "Crypto.com Arena"                        // Can change
  city: "Los Angeles"                             // Can change
  deprecatedAt: nil                               // Set when demolished

CanonicalTeam:
  canonicalId: "team_mlb_athletics"              // Immutable
  stadiumCanonicalId: "stadium_mlb_las_vegas"   // Can change (relocation)
  city: "Las Vegas"                              // Can change
  deprecatedAt: nil                              // Set if team ceases

// Historical name tracking
StadiumAlias:
  aliasName: "staples center"                    // Lowercase for matching
  stadiumCanonicalId: "stadium_nba_los_angeles_lakers"
  validFrom: 2021-01-01
  validUntil: 2022-12-25

TeamAlias:
  aliasValue: "Oakland"
  aliasType: .city
  teamCanonicalId: "team_mlb_athletics"
  validUntil: 2024-12-31
```

### User Data (Local Only)

```swift
StadiumVisit:
  stadiumId: String           // Canonical ID - stable reference
  stadiumNameAtVisit: String  // Frozen at visit time for history
  homeTeamId: String?         // Canonical ID
  awayTeamId: String?         // Canonical ID
  homeTeamName: String?       // Frozen at visit time
  awayTeamName: String?       // Frozen at visit time

Achievement:
  achievementTypeId: String   // League structure ID (e.g., "mlb_al_west")
  sport: String?
  visitIdsSnapshot: Data      // UUIDs of StadiumVisits - immutable
```

## Scenario Handling

### Stadium Renames

**Example:** Staples Center → Crypto.com Arena (December 2021)

**What happens:**
1. CloudKit updates `CanonicalStadium.name` to "Crypto.com Arena"
2. CloudKit adds `StadiumAlias` for "Staples Center" with validity dates
3. Next sync updates local SwiftData
4. `canonicalId` remains `"stadium_nba_los_angeles_lakers"`

**User impact:** None
- Existing `StadiumVisit.stadiumId` still resolves correctly
- `StadiumVisit.stadiumNameAtVisit` preserves "Staples Center" for historical display
- Searching "Staples Center" still finds the stadium via alias lookup

### New Stadium Built

**Example:** New Las Vegas A's stadium opens in 2028

**What happens:**
1. CloudKit adds new `CanonicalStadium` record with new canonical ID
2. Next sync creates record in SwiftData
3. Deterministic UUID generated: `SHA256(canonicalId) → UUID`
4. Stadium appears in app automatically

**User impact:** None
- New stadium available for visits and achievements
- No migration needed

### Team Relocates

**Example:** Oakland A's → Las Vegas A's (2024-2028)

**What happens:**
1. CloudKit updates `CanonicalTeam`:
   - `canonicalId` stays `"team_mlb_athletics"` (never changes)
   - `stadiumCanonicalId` updated to Las Vegas stadium
   - `city` updated to "Las Vegas"
2. CloudKit adds `TeamAlias` for "Oakland" with end date
3. Old Oakland Coliseum gets `deprecatedAt` timestamp (soft delete)

**User impact:** None
- Old visits preserved: `StadiumVisit.stadiumId` = Oakland Coliseum (still valid)
- Old visits show historical context: "Oakland A's at Oakland Coliseum"
- Achievements adapt: "Complete AL West" now requires Las Vegas stadium

### Stadium Demolished / Team Ceases

**What happens:**
- Record gets `deprecatedAt` timestamp (soft delete, never hard delete)
- Filtered from active queries: `predicate { $0.deprecatedAt == nil }`
- Historical data fully preserved

**User impact:** None
- Visits remain in history, just not in active stadium lists
- Achievements not revoked - you earned it, you keep it

## Identity Resolution

`StadiumIdentityService` handles all lookups:

```swift
// Find canonical ID from any name (current or historical)
StadiumIdentityService.shared.canonicalId(forName: "Staples Center")
// → "stadium_nba_los_angeles_lakers"

// Get current display name from canonical ID
StadiumIdentityService.shared.currentName(forCanonicalId: "stadium_nba_los_angeles_lakers")
// → "Crypto.com Arena"

// Get all historical names
StadiumIdentityService.shared.allNames(forCanonicalId: "stadium_nba_los_angeles_lakers")
// → ["Crypto.com Arena", "Staples Center", "Great Western Forum"]
```

## Sync Safety

During `CanonicalSyncService.mergeStadium()`:

```swift
if let existing = try context.fetch(descriptor).first {
    // PRESERVE user customizations
    let savedNickname = existing.userNickname
    let savedFavorite = existing.isFavorite

    // UPDATE system fields only
    existing.name = remote.name
    existing.city = remote.city
    // canonicalId is NOT updated - it's immutable

    // RESTORE user customizations
    existing.userNickname = savedNickname
    existing.isFavorite = savedFavorite
}
```

## Impact Summary

| Scenario | User Visits | Achievements | Historical Display |
|----------|-------------|--------------|-------------------|
| Stadium rename | ✅ Preserved | ✅ Preserved | Shows name at visit time |
| New stadium | N/A | Available to earn | N/A |
| Team relocates | ✅ Preserved | ✅ Logic adapts | Shows team + old stadium |
| Stadium demolished | ✅ Preserved | ✅ Not revoked | Marked deprecated, visible in history |

## Key Principle

**Immutable references, mutable display.** User data stores only canonical IDs. Display names are resolved at read time via `StadiumIdentityService`. This means we can update the real world without ever migrating user data.
