# Charge Curve Storage Operation

Stocarea şi retrieval-ul curbelor de încărcare în persistență.

## Responsabilități

- Persistență curbe în Core Data (sesiuni + measurements)
- Sincronizare curbe în iCloud (CloudKit)
- Compresie date (sampling, agregare)
- Cleanup și archiving sesiuni vechi

## Arhitectură

### Core Data entities

```
ChargeSession
├── id: UUID
├── chargedDeviceID: UUID
├── startTime: Date
├── endTime: Date
├── startBatteryPercent: Double?
├── endBatteryPercent: Double?
├── totalEnergyWh: Double
└── checkpoints: [ChargeCheckpoint]

ChargeCheckpoint (measurement)
├── id: UUID
├── sessionID: UUID
├── timestamp: Date
├── voltage: Double
├── current: Double
├── power: Double
├── temperature: Double?
└── batteryPercent: Double?
```

### Storage layers

1. **In-memory cache**: Recent sessions (last 24h)
2. **Local Core Data**: All sessions
3. **iCloud CloudKit**: Synced sessions (optional, per user)
4. **Archive**: Old sessions (JSON export, local only)

```
Memory cache (24h)
       ↓
   Core Data (local)
   /           \
  ↓             ↓
 iCloud      Archive (JSON)
(CloudKit)   (5+ years old)
```

## Invarianţi

- **MUST**: Fiecare `ChargeSession` are `id` unic
- **MUST**: Fiecare `ChargeCheckpoint` are reference valid la `sessionID`
- **MUST**: Checkpoints ordonate cronologic în sesiune
- **MUST**: `startTime < endTime` pentru completed sessions
- **MUST**: CloudKit sync preserve integrity (no partial sessions)
- **SHOULD**: Sesiuni < 5 minute nu se sincronizează (noise)
- **MAY**: Archive sesiuni > 5 ani local-only

## Save flow

### 1. In-memory accumulation

```swift
let session = meter.startChargeRecord(for: device)
// session object in-memory
// accumulate measurements în session.measurements array

while isCharging {
    let measurement = meter.lastDataPoint
    session.addMeasurement(measurement)  // In-memory
}
```

**Properties:**
- Rapid (no I/O)
- Loss on app crash
- Limits: max 1MB per session (~10k measurements @ 1Hz)

### 2. Flush to Core Data

```swift
func endChargeRecord(_ session: ChargeRecord) {
    // 1. Isolate valid curve
    let isolated = isolateChargeRange(session.measurements)
    
    // 2. Save to Core Data
    let coreDataSession = ChargeSession()
    coreDataSession.id = session.id
    coreDataSession.chargedDeviceID = device.id
    coreDataSession.startTime = isolated.realStartTime
    coreDataSession.endTime = isolated.realEndTime
    coreDataSession.totalEnergyWh = calculateEnergy(isolated.measurements)
    
    // 3. Save checkpoints
    for measurement in isolated.measurements {
        let checkpoint = ChargeCheckpoint()
        checkpoint.timestamp = measurement.timestamp
        checkpoint.voltage = measurement.voltage
        checkpoint.current = measurement.current
        checkpoint.power = measurement.power
        // ... other fields
        coreDataSession.checkpoints.append(checkpoint)
    }
    
    // 4. Persist
    managedObjectContext.insert(coreDataSession)
    try? managedObjectContext.save()
}
```

**Guarantees:**
- ACID transaction
- Survives app crash
- Instantly queryable

### 3. CloudKit sync (async)

```swift
// NSPersistentCloudKitContainer handles automatically
// New/modified ChargeSession → CloudKit
// Happens in background, doesn't block UI
```

**Flow:**
```
Core Data save
  ↓
NSPersistentCloudKitContainer observes change
  ↓
Serializes to CloudKit record
  ↓
Uploads in background (when network available)
  ↓
Notifies on success/failure
```

**CloudSync constraints:**
- **MUST**: Sessions < 5 min nu se syncă (noise filter)
- **MUST**: Checkpoints limitate la max 1000 per CloudKit record
- **SHOULD**: Aggregate samples dacă > 1000 checkpoints

## Load flow

### 1. Query Core Data

```swift
func loadSessions(for device: ChargedDevice) -> [ChargeSession] {
    let request: NSFetchRequest<ChargeSession> = ChargeSession.fetchRequest()
    request.predicate = NSPredicate(format: "chargedDeviceID == %@", device.id as NSUUID)
    request.sortDescriptors = [NSSortDescriptor(key: "startTime", ascending: false)]
    
    let sessions = try? managedObjectContext.fetch(request)
    return sessions ?? []
}

func loadCheckpoints(for session: ChargeSession) -> [ChargeCheckpoint] {
    let request: NSFetchRequest<ChargeCheckpoint> = ChargeCheckpoint.fetchRequest()
    request.predicate = NSPredicate(format: "sessionID == %@", session.id as NSUUID)
    request.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)]
    
    let checkpoints = try? managedObjectContext.fetch(request)
    return checkpoints ?? []
}
```

**Performance:**
- O(log n) for device lookup (indexed)
- O(k) for checkpoint load (k = checkpoint count, typically 1000-5000)

### 2. Reconstruct curve

```swift
func reconstructCurve(from session: ChargeSession) -> ChargeCurve {
    let checkpoints = loadCheckpoints(for: session)
    
    return ChargeCurve(
        startTime: session.startTime,
        endTime: session.endTime,
        totalEnergy: session.totalEnergyWh,
        measurements: checkpoints.map { cp in
            Measurement(
                timestamp: cp.timestamp,
                voltage: cp.voltage,
                current: cp.current,
                power: cp.power
            )
        }
    )
}
```

### 3. Cache in memory

```swift
// Keep last 24h in cache
var recentCurves: [UUID: ChargeCurve] = [:]

func cachedCurve(for sessionID: UUID) -> ChargeCurve? {
    if let cached = recentCurves[sessionID] {
        return cached  // Memory hit
    }
    
    // Core Data miss → fetch + cache
    guard let coreDataSession = loadSessionFromCoreData(sessionID) else {
        return nil
    }
    
    let curve = reconstructCurve(from: coreDataSession)
    recentCurves[sessionID] = curve
    return curve
}
```

## Compression strategies

### For frequent access (last 7 days)

**Store:** All 1Hz measurements
**Cost:** ~1000 checkpoints per session

### For occasional access (7-365 days)

**Strategy:** Downsample to 10Hz
```swift
func downsampleCurve(measurements: [Measurement], factor: Int) -> [Measurement] {
    return measurements.enumerated()
        .filter { $0.offset % factor == 0 }
        .map { $0.element }
}
```

**Reduction:** 10x fewer points
**Loss:** Minimal (still captures curve shape)
**Cost:** ~100 checkpoints per session

### For archive (> 1 year)

**Strategy:** Store metadata only
```swift
struct SessionMetadata {
    let id: UUID
    let chargedDeviceID: UUID
    let startTime: Date
    let endTime: Date
    let totalEnergyWh: Double
    let peakPowerW: Double
    let averagePowerW: Double
    // No measurements stored
}
```

**Cost:** Minimal (constant per session)
**Retrieval:** Summary only, not full curve

## CloudKit constraints

### Record size limits

- CloudKit record: max 4 MB
- Checkpoints JSON: ~100 bytes per checkpoint
- Max checkpoints per record: ~40k
- Typical session: 1000-5000 checkpoints

### Sync strategy

```swift
func syncSessionsToCloudKit() {
    let allSessions = loadSessionsFromCoreData()
    
    for session in allSessions {
        // Filter: only sessions > 5 minutes
        guard session.duration >= 300 else { continue }
        
        // Aggregate: if checkpoints > 2000, downsample
        var checkpoints = loadCheckpoints(for: session)
        if checkpoints.count > 2000 {
            checkpoints = downsampleCurve(checkpoints, factor: 2)
        }
        
        // Push to CloudKit (NSPersistentCloudKitContainer handles)
    }
}
```

**MUST**: Filtrare sesiuni mici (< 5 min)
**SHOULD**: Agregate checkpoint-uri dacă > 2000
**MAY**: Archive > 1 an local-only

## API Public

### Save

```swift
// Save completed session
func saveChargeSession(_ session: ChargeRecord)
// Isolates curve → persists to Core Data → CloudKit async

// Append checkpoint
func appendCheckpoint(_ measurement: Measurement, to sessionID: UUID)
// In-memory only, flushed on session end
```

### Load

```swift
// Query by device
func loadSessions(for device: ChargedDevice) -> [ChargeSession]

// Load single session
func loadSession(id: UUID) -> ChargeSession?

// Load curve with checkpoints
func loadFullCurve(sessionID: UUID) -> ChargeCurve?

// Load metadata only (fast)
func loadSessionMetadata(sessionID: UUID) -> SessionMetadata?
```

### Cleanup

```swift
// Archive old sessions (local)
func archiveOldSessions(olderThan: Date) -> Int
// Returns count archived

// Cleanup from iCloud
func deleteFromCloudKit(sessionID: UUID)
// MUST: permanent (CloudKit deletion, can't undo)
```

## Comportamente critice

### Concurrent saves

```
User: Start session on meter A
App: Saves session A
User: Also start session on meter B
App: Saves session B
⟹ Both saved (transactions independent)
```

**MUST**: Core Data transactions isolate (ACID)
**MUST**: CloudKit syncs maintain order

### CloudKit unavailable

```
Device offline
App saves to Core Data ✓
CloudKit sync queued
User goes online
Sync resumes automatically
⟹ Transparent
```

**MUST**: Queue pending changes
**SHOULD**: Retry with backoff
**MUST**: Persist queue to disk (survive app restart)

### Duplicate on restore

```
Device A: 100 sessions synced to iCloud
User: Restore from backup
Device B: Pulls 100 sessions
Device A: Restarts, sees 100 sessions (already synced)
⟹ Deduplication needed
```

**MUST**: Deduplicate by session ID
**SHOULD**: Keep most recent version

## Testare

### Unit tests

```swift
test_saveSession_PersistsToCoreData()
test_loadSessions_ByDevice()
test_loadCheckpoints_Ordered()
test_downsampleCurve_ReducesFactor()
test_cloudSyncFilters_SessionsUnder5Min()
test_archiveOldSessions_By Date()
test_deduplicateOnRestore()
```

### Integration tests

- [ ] Save session → reload → same data
- [ ] CloudKit sync without network (queued)
- [ ] Resume CloudKit sync when online
- [ ] Downsample 7+ day old sessions
- [ ] Archive 1+ year old sessions
- [ ] Delete from CloudKit (permanent)

## Performance considerations

| Operation | Time | Notes |
|---|---|---|
| Save session | < 100ms | Core Data write |
| Load 100 sessions | < 50ms | Indexed query |
| Load curve (1000 pts) | < 200ms | Reconstruct array |
| CloudKit sync | ~1-5s | Network dependent |
| Downsample 5000 pts | < 20ms | CPU bound |

## Dependenţe

- Core Data: NSManagedObjectContext
- CloudKit: NSPersistentCloudKitContainer
- [Charging Monitoring](./ChargingMonitoring.md): input sessions
- [Charge Curve Isolation](./ChargeCurveIsolation.md): isolated data
- File system: archiving

## References

- [Charge Session Integrity](../Charge%20Session%20Integrity%20and%20Conflict%20Healing.md)
- Core Data Performance Tuning: https://developer.apple.com/videos/play/wwdc2021/10190/
- CloudKit limits: https://developer.apple.com/icloud/cloudkit/
