# CloudKit Sync Schema

## Overview
USB Meter utilizează `NSPersistentCloudKitContainer` pentru a sincroniza setările dispozitivelor Bluetooth pe mai multe device-uri iOS/iPadOS via iCloud CloudKit.

**Container CloudKit:** `iCloud.ro.xdev.USB-Meter`

---

## Core Data Entity: DeviceSettings

### Purpose
Stocare centralizată a setărilor și metadatelor de conexiune pentru fiecare contor Bluetooth, cu sincronizare automată pe toate device-urile utilizatorului.

### Device Name Source
- **All platforms (iOS, iPadOS, macOS Catalyst, iPad on Mac):** `ProcessInfo.processInfo.hostName`
  - Examples: "iPhone-User", "MacBook-Pro.local", "iPad-WiFi"
  - Rationale: Platform-agnostic, avoids device model names ("iPad", "iPhone" are useless)

### Attributes

| Atribut | Tip | Optional | Descriere |
|---------|-----|----------|-----------|
| **macAddress** | String | YES | Adresa MAC unică a contorului BT (cheie candidat pentru deduplicare) |
| **meterName** | String | YES | Renumire personalizată a contorului (ex: "Kitchen Meter") |
| **tc66TemperatureUnit** | String | YES | Preferință unitate temperatură pentru TC66C (raw value enum: "celsius"/"fahrenheit") |
| **modelType** | String | YES | Tip model contor ("UM25C", "UM34C", "TC66C") |
| **connectedByDeviceID** | String | YES | UUID-ul device-ului iOS care l-a conectat ultima dată |
| **connectedByDeviceName** | String | YES | Numele device-ului iOS care l-a conectat |
| **connectedAt** | Date | YES | Timestamp al celei mai recente conexiuni |
| **connectedExpiryAt** | Date | YES | TTL pentru lock-ul de conexiune (alt device poate prelua dacă a expirat) |
| **lastSeenAt** | Date | YES | Timestamp al ultimei avertisment BT din device |
| **lastSeenByDeviceID** | String | YES | UUID device-ul iOS care l-a văzut ultima dată |
| **lastSeenByDeviceName** | String | YES | Nume device-ul iOS care l-a văzut |
| **lastSeenPeripheralName** | String | YES | Numele periferalei așa cum raporta dispozitivul |
| **updatedAt** | Date | YES | Timestamp al ultimei actualizări (audit trail) |

### CloudKit Mapping
Fiecare atribut Core Data → câmp CloudKit în record-ul `DeviceSettings`:
- Sincronizare automată bidirecțională
- Lightweight migration pentru schimbări de schemă
- Conflict resolution: **NSMergeByPropertyStoreTrumpMergePolicy** (CloudKit wins pe changed properties)

---

## Constraints

### ❌ **NOT** Used (v1 - v2):
- `uniquenessConstraint` pe `macAddress` — **incompatibil cu NSPersistentCloudKitContainer** (genereaza erori silentioase)

### ✅ Applied:
- **Logical deduplication** per macAddress în `rebuildCanonicalStoreIfNeeded`
  - Ruleaza o dată pe device (verifică `UserDefaults` cu key `"cloudStoreRebuildVersion.{n}"`)
  - Merges conflicting records pe baza timestamp-urilor

---

## Data Flow

```
┌─────────────────────────────────────────────────────────────────┐
│                      Device A (iPhone)                          │
├─────────────────────────────────────────────────────────────────┤
│  User connects UM25C (MAC: aa:bb:cc:dd:ee:ff)                  │
│         ↓                                                        │
│  Meter.swift → appData.publishMeterConnection(mac, type)       │
│         ↓                                                        │
│  CloudDeviceSettingsStore.setConnection()                      │
│    - Creates / Updates DeviceSettings record                   │
│    - Sets connectedByDeviceID = Device A UUID                 │
│    - connectedAt = now                                         │
│    - connectedExpiryAt = now + 120s                           │
│         ↓                                                        │
│  NSPersistentCloudKitContainer saves to Core Data              │
│         ↓                                                        │
│  CloudKit framework (automatic sync):                          │
│    - Pushes record modificare → iCloud server                  │
│    - Records: [CKRecord with RecordID "DeviceSettings/mac"]   │
│         ↓                                                        │
│  iCloud Server → Device B (iPad)                               │
│         ↓                                                        │
│  Device B receives remote change notification                  │
│  (NSPersistentStoreRemoteChangeNotificationPostOptionKey)      │
│         ↓                                                        │
│  AppData.reloadSettingsFromCloudStore()                        │
│    - Fetches updated record din local Core Data                │
│    - Sees connectedByDeviceID = Device A UUID                 │
│    - Checks connectedExpiryAt (not expired)                   │
│    - Knows someone else is connected                          │
│         ↓                                                        │
│  UI updates: shows "Connected on iPhone"                       │
└─────────────────────────────────────────────────────────────────┘
```

---

## Conflict Resolution Scenarios

### Scenario 1: Simultaneous Connection Attempt
**Device A** și **Device B** conectează același contor ~același timp.

```
Device A:                          Device B:
t=0.0s: setConnection()           t=0.1s: setConnection()
  - connectedByDeviceID = A         - connectedByDeviceID = B
  - connectedAt = 0.0s              - connectedAt = 0.1s
       ↓ save                             ↓ save
  CloudKit pushes A's record    CloudKit pushes B's record
       ↓                              ↓
       └──────→ iCloud Server ←──────┘
               (concurrent writes)

Merge Strategy:
  - NSMergeByPropertyStoreTrumpMergePolicy
  - Latest CloudKit record "wins" per property
  - Result: B's connectedByDeviceID + connectedAt win
    (mai recent timestamp)
```

### Scenario 2: Reconnection After Expiry
**Device A** conectează la t=0. Expiry la t=120. **Device B** incearcă conexiune la t=150.

```
t=0s:   Device A connects (connectedAt=0, expiry=120)
t=120s: Connection expires
t=150s: Device B checks:
        if (connectedExpiryAt < now):
          // Lock expired, can connect
        setConnection() → Device B becomes new owner
```

---

## Deduplication Logic (v3+)

Rulează la `AppDelegate.didFinishLaunchingWithOptions`:

```swift
rebuildCanonicalStoreIfNeeded(version: 3)
  1. Fetch ALL DeviceSettings records
  2. Group by normalized macAddress
  3. Per group:
     - Select "winner" (prefer: more recent updatedAt, more data fields populated)
     - Merge other records INTO winner
     - Delete duplicates (only delete-uri, no inserts)
  4. Save context → CloudKit deletes propagate
  5. Mark in UserDefaults as "done for v3"
```

**De ce in-place?** Pentru a nu cauza "delete storm":
- ❌ Old way: Delete ALL + Insert ALL → alt device primește DELETE-uri înainte de INSERT-uri → pierde date
- ✅ New way: Update IN-PLACE + Delete only duplicates → record ID-ul DeviceSettings este păstrat → sincronizare progresivă

---

## Version History

| Model | Version | Changes |
|-------|---------|---------|
| USB_Meter.xcdatamodel | 1 | Original schema cu `uniquenessConstraint` |
| USB_Meter 2.xcdatamodel | 2 | ❌ Removed constraint (but rebuild was destructive) |
| USB_Meter 2.xcdatamodel | 3 | ✅ Rebuilt with in-place merge strategy |

Migration path:
```
v1 → v2 (lightweight, auto) → v3 (rebuild in-place, UserDefaults gated)
```
