# CloudKit Sync Mechanism

## Architecture Overview

```
┌──────────────────────────────────────────────────────────────┐
│                        App Layer                            │
│  Meter.swift (UI)  ←→  BluetoothManager.swift (BT comms)  │
└───────────┬────────────────────────────┬────────────────────┘
            │                            │
            └───────────┬────────────────┘
                        │
              ┌─────────▼──────────┐
              │ AppData.swift      │
              │  (Orchestrator)    │
              └──────────┬─────────┘
                         │
        ┌────────────────┼────────────────┐
        │                │                │
   ┌────▼─────┐   ┌─────▼──────┐   ┌────▼──────────┐
   │ CloudDeviceSettingsStore   │   │ Bluetooth    │
   │ (Core Data Layer)          │   │ Persistence  │
   └────┬─────┘                 │   └──────────────┘
        │                       │
   ┌────▼────────────────────────────┐
   │ NSManagedObjectContext           │
   │ (viewContext)                    │
   └────┬─────────────────────────────┘
        │
   ┌────▼──────────────────────────────────────────┐
   │ NSPersistentCloudKitContainer                  │
   │ - Automatic bi-directional sync               │
   │ - Handles conflicts via merge policy          │
   │ - Remote change notifications                │
   └────┬─────────────────────┬────────────────────┘
        │                     │
   ┌────▼──────┐    ┌────────▼─────────────────┐
   │ Local DB  │    │ CloudKit Framework       │
   │ SQLite    │    │ - Push notifications     │
   │           │    │ - Persistent history    │
   └───────────┘    └────────┬──────────────────┘
                             │
                        ┌────▼────────────┐
                        │ iCloud Server   │
                        │ CloudKit DB     │
                        └────┬────────────┘
                             │
                        (Sync'd to other devices)
```

---

## Sync Flow - Step by Step

### Local Write Flow

```
1. User Action (connect meter)
   │
2. Meter.swift → operationalState.didSet
   │
3. appData.publishMeterConnection(mac: String, modelType: String)
   │
4. CloudDeviceSettingsStore.setConnection(macAddress:, modelType:)
   │
   ├─ Fetch existing DeviceSettings for macAddress
   │  • If nil: create new NSManagedObject
   │  • If exists: update it
   │
5. Update attributes:
   │  • connectedByDeviceID = UIDevice.current.identifierForVendor
   │  • connectedByDeviceName = UIDevice.current.name
   │  • connectedAt = Date()
   │  • connectedExpiryAt = Date.now + 120 seconds
   │  • updatedAt = Date()
   │
6. appData.persistentContainer.viewContext.save()
   │
7. NSPersistentCloudKitContainer intercepts save:
   │  • Detects changes to DeviceSettings entity
   │  • Serializes to CKRecord ("DeviceSettings/[macAddress]")
   │
8. CloudKit framework:
   │  • Queues for upload (background)
   │  • Sends to iCloud servers
   │
9. Remote device:
   │  • Receives NSPersistentStoreRemoteChangeNotification
   │  • Automatically merges into local Core Data
   │  • AppData detects change → reloadSettingsFromCloudStore()
   │  • UI updates via @Published updates
```

---

### Receiving Remote Changes

```
1. iCloud Server discovers change
   │
2. Sends data to Device B via CloudKit
   │
3. iOS system fires:
   NSPersistentStoreRemoteChangeNotificationPostOptionKey
   │
4. AppData.setupRemoteChangeNotificationObserver():
   @objc private func remoteStoreDidChange() {
     // This runs on background thread!
     reloadSettingsFromCloudStore()
   }
   │
5. reloadSettingsFromCloudStore() fetches fresh data:
   │
   ├─ cloudStore.fetch(for: macAddress) [async per-device]
   │
6. Updates local @Published properties
   │  → triggers UI re-render in SwiftUI
   │
7. Meter.swift observes changes:
   │  → updates connection state badges
   │  → shows "Connected on [Device Name]" etc.
```

---

## AppData: Core Sync Methods

### Device Name Handling (AppData.myDeviceName)

```swift
static let myDeviceName: String = Self.getDeviceName()

private static func getDeviceName() -> String {
    #if os(macOS)
    // On macOS (Catalyst, iPad App on Mac), use hostname
    let hostname = ProcessInfo.processInfo.hostName
    return hostname.trimmingCharacters(in: .whitespacesAndNewlines)
    #else
    // On iOS/iPadOS, use device name
    return UIDevice.current.name.trimmingCharacters(in: .whitespacesAndNewlines)
    #endif
}
```

**Why:** `UIDevice.current.name` returns "iPad" on macOS, which is wrong. Using `hostname` gives correct names like "MacBook-Pro.local" or "iMac.local".

### `persistentContainer` (AppDelegate.swift)

```swift
lazy var persistentContainer: NSPersistentCloudKitContainer = {
    let container = NSPersistentCloudKitContainer(name: "CKModel")
    if let description = container.persistentStoreDescriptions.first {
        description.cloudKitContainerOptions =
            NSPersistentCloudKitContainerOptions(
                containerIdentifier: "iCloud.ro.xdev.USB-Meter"
            )
        // Enable historical tracking for conflict resolution
        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        // Enable remote change notifications
        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
    }
    container.viewContext.automaticallyMergesChangesFromParent = true
    container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
    return container
}()
```

**Key configs:**
- `automaticallyMergesChangesFromParent = true`: Auto-merge CloudKit changes without explicit fetch
- `NSMergeByPropertyStoreTrumpMergePolicy`: CloudKit always wins on property conflicts

### `activateCloudDeviceSync` (SceneDelegate.swift)

```swift
appData.activateCloudDeviceSync(context: appDelegate.persistentContainer.viewContext)
```

**Does:**
1. Sets up remote change observers
2. Triggers first `reloadSettingsFromCloudStore()` to load persisted CloudKit data
3. Starts polling for connection expiry checks

### `publishMeterConnection` (AppData.swift)

```swift
public func publishMeterConnection(macAddress: String, modelType: String) {
    DispatchQueue.global(qos: .userInitiated).async { [weak self] in
        self?.cloudStore.setConnection(macAddress: macAddress, modelType: modelType)
    }
}
```

**Called from:** `Meter.swift` when `operationalState` changes to `.peripheralConnected`

### `clearMeterConnection` (AppData.swift)

```swift
public func clearMeterConnection(macAddress: String) {
    DispatchQueue.global(qos: .userInitiated).async { [weak self] in
        self?.cloudStore.clearConnection(macAddress: macAddress)
    }
}
```

**Called from:** `Meter.swift` when `operationalState` → `.offline` / `.peripheralNotConnected`

### `reloadSettingsFromCloudStore` (AppData.swift)

```swift
private func reloadSettingsFromCloudStore() {
    // Fetch all DeviceSettings records
    // Update internal @Published state
    // Publish changes via objectWillChange
}
```

**Called from:**
- `activateCloudDeviceSync` (initial load)
- Remote change notification handler (when CloudKit syncs changes)
- Periodic check for connection expiry

---

## CloudDeviceSettingsStore: Private Implementation

```swift
private class CloudDeviceSettingsStore {
    let context: NSManagedObjectContext
    let entityName = "DeviceSettings"

    func setConnection(macAddress: String, modelType: String) {
        context.performAndWait {
            // 1. Normalize MAC
            let mac = normalizedMACAddress(macAddress)

            // 2. Fetch or create
            let request = NSFetchRequest<NSManagedObject>(entityName: entityName)
            request.predicate = NSPredicate(format: "macAddress == %@", mac)
            var object = try? context.fetch(request).first
            if object == nil {
                object = NSManagedObject(entity: ..., insertInto: context)
            }

            // 3. Update connection metadata
            object?.setValue(mac, forKey: "macAddress")
            object?.setValue(modelType, forKey: "modelType")
            object?.setValue(UIDevice.current.identifierForVendor?.uuidString, forKey: "connectedByDeviceID")
            object?.setValue(UIDevice.current.name, forKey: "connectedByDeviceName")
            object?.setValue(Date(), forKey: "connectedAt")
            object?.setValue(Date().addingTimeInterval(120), forKey: "connectedExpiryAt")
            object?.setValue(Date(), forKey: "updatedAt")

            // 4. Save → triggers CloudKit sync
            try? context.save()
        }
    }

    func fetch(for macAddress: String) -> DeviceSettingsRecord? {
        // Similar to setConnection, but returns data only
    }
}
```

---

## Connection Lifecycle

```
Device A Connects                Expiry Timer Started
    │                                    │
    ▼                                    ▼
┌─────────────────────────┐    ┌──────────────────┐
│ DeviceSettings created  │    │  120 seconds     │
│ connectedAt = now       │    │  (hardcoded TTL) │
│ connectedExpiryAt=now+2m│◄───┴──────────────────┘
└─────────────────────────┘
    │
    │ [CloudKit syncs]
    │
    ▼
Device B detects via notification:
  connectedByDeviceID ≠ my device → "Locked by Device A"

After 120 seconds:
  if (now > connectedExpiryAt) {
    // Lock expired
    // Device B can attempt new connection
  }
```

---

## Error Handling & Recovery

### Scenario: CloudKit Unavailable
- App detects no iCloud account → operations on viewContext proceed locally
- Changes queue locally
- When CloudKit available → sync resumes automatically

### Scenario: Duplicate Records (Pre-v3)
- `rebuildCanonicalStoreIfNeeded(version: 3)` runs at app launch
- Detects and merges duplicates in-place
- Marks as done in UserDefaults

### Scenario: Sync Conflicts
- Example: Device A and B both update `meterName` simultaneously
- `NSMergeByPropertyStoreTrumpMergePolicy` applies: **latest CloudKit wins**
- User may see their local change revert briefly (acceptable UX for rare case)

---

## Performance Considerations

| Operation | Thread | Frequency | Impact |
|-----------|--------|-----------|--------|
| `setConnection()` | Background (global QoS) | Per meter connection | Minimal (~5-10ms) |
| `reloadSettingsFromCloudStore()` | Main (UI updates) | On remote change + periodic | ~50-100ms for <10 meters |
| `rebuildCanonicalStoreIfNeeded()` | Main (view context) | Once per app version | ~200ms (one-time) |
| CloudKit sync (background) | System | Continuous | Battery: negligible |

---

## Testing Checklist

- [ ] Connect meter on Device A, verify "Connected on A" badge on Device B within 5s
- [ ] Disconnect on A, verify badge clears on B
- [ ] Rename meter on A, verify name updates on B
- [ ] Set TC66 temp unit on A, verify syncs to B
- [ ] Airplane mode on A → CloudKit queues changes → reconnect → syncs ✓
- [ ] Delete app data on A, reinstall → rebuilds from CloudKit ✓
- [ ] Simulated multi-device conflict (manual Core Data edit) → resolves via merge policy ✓
