import 'dart:convert'; import 'supplement.dart'; import 'supplement_intake.dart'; import 'sync_enums.dart'; /// Model representing the complete sync data structure /// This is what gets serialized to JSON and synced via WebDAV class SyncData { final int version; final String deviceId; final String deviceName; final DateTime syncTimestamp; final List supplements; final List intakes; final Map metadata; const SyncData({ required this.version, required this.deviceId, required this.deviceName, required this.syncTimestamp, required this.supplements, required this.intakes, this.metadata = const {}, }); /// Convert sync data to JSON map Map toJson() { return { 'version': version, 'deviceId': deviceId, 'deviceName': deviceName, 'syncTimestamp': syncTimestamp.toIso8601String(), 'supplements': supplements.map((s) => s.toMap()).toList(), 'intakes': intakes.map((i) => i.toMap()).toList(), 'metadata': metadata, }; } /// Create sync data from JSON map factory SyncData.fromJson(Map json) { return SyncData( version: json['version'] ?? 1, deviceId: json['deviceId'] ?? '', deviceName: json['deviceName'] ?? 'Unknown Device', syncTimestamp: json['syncTimestamp'] != null ? DateTime.parse(json['syncTimestamp']) : DateTime.now(), supplements: (json['supplements'] as List? ?? []) .map((s) => Supplement.fromMap(s as Map)) .toList(), intakes: (json['intakes'] as List? ?? []) .map((i) => SupplementIntake.fromMap(i as Map)) .toList(), metadata: json['metadata'] as Map? ?? {}, ); } /// Convert sync data to JSON string String toJsonString() { return jsonEncode(toJson()); } /// Create sync data from JSON string factory SyncData.fromJsonString(String jsonString) { final json = jsonDecode(jsonString) as Map; return SyncData.fromJson(json); } /// Create a copy with updated values SyncData copyWith({ int? version, String? deviceId, String? deviceName, DateTime? syncTimestamp, List? supplements, List? intakes, Map? metadata, }) { return SyncData( version: version ?? this.version, deviceId: deviceId ?? this.deviceId, deviceName: deviceName ?? this.deviceName, syncTimestamp: syncTimestamp ?? this.syncTimestamp, supplements: supplements ?? this.supplements, intakes: intakes ?? this.intakes, metadata: metadata ?? this.metadata, ); } @override String toString() { return 'SyncData(version: $version, deviceId: $deviceId, ' 'deviceName: $deviceName, syncTimestamp: $syncTimestamp, ' 'supplements: ${supplements.length}, intakes: ${intakes.length})'; } } /// Model representing a sync conflict class SyncConflict { final String syncId; final ConflictType type; final DateTime localTimestamp; final DateTime remoteTimestamp; final Map localData; final Map remoteData; final ConflictResolutionStrategy? suggestedResolution; const SyncConflict({ required this.syncId, required this.type, required this.localTimestamp, required this.remoteTimestamp, required this.localData, required this.remoteData, this.suggestedResolution, }); Map toJson() { return { 'syncId': syncId, 'type': type.name, 'localTimestamp': localTimestamp.toIso8601String(), 'remoteTimestamp': remoteTimestamp.toIso8601String(), 'localData': localData, 'remoteData': remoteData, 'suggestedResolution': suggestedResolution?.name, }; } factory SyncConflict.fromJson(Map json) { return SyncConflict( syncId: json['syncId'], type: ConflictType.values.firstWhere( (e) => e.name == json['type'], orElse: () => ConflictType.modification, ), localTimestamp: DateTime.parse(json['localTimestamp']), remoteTimestamp: DateTime.parse(json['remoteTimestamp']), localData: json['localData'] as Map, remoteData: json['remoteData'] as Map, suggestedResolution: json['suggestedResolution'] != null ? ConflictResolutionStrategy.values.firstWhere( (e) => e.name == json['suggestedResolution'], orElse: () => ConflictResolutionStrategy.manual, ) : null, ); } /// Get human-readable description of the conflict String get description { switch (type) { case ConflictType.modification: return 'Item modified on both devices'; case ConflictType.deletion: return 'Item deleted on one device, modified on another'; case ConflictType.creation: return 'Item created with same ID on both devices'; } } /// Determine if local version is newer bool get isLocalNewer => localTimestamp.isAfter(remoteTimestamp); /// Determine if remote version is newer bool get isRemoteNewer => remoteTimestamp.isAfter(localTimestamp); /// Check if timestamps are identical bool get haveSameTimestamp => localTimestamp.isAtSameMomentAs(remoteTimestamp); } /// Types of sync conflicts enum ConflictType { /// Both local and remote versions were modified modification, /// One version was deleted while the other was modified deletion, /// Same item was created on both devices creation, } /// Model for sync operation results class SyncResult { final bool success; final DateTime timestamp; final SyncOperationStatus status; final String? error; final List conflicts; final SyncStatistics statistics; const SyncResult({ required this.success, required this.timestamp, required this.status, this.error, this.conflicts = const [], this.statistics = const SyncStatistics(), }); Map toJson() { return { 'success': success, 'timestamp': timestamp.toIso8601String(), 'status': status.name, 'error': error, 'conflicts': conflicts.map((c) => c.toJson()).toList(), 'statistics': statistics.toJson(), }; } factory SyncResult.fromJson(Map json) { return SyncResult( success: json['success'] ?? false, timestamp: DateTime.parse(json['timestamp']), status: SyncOperationStatus.values.firstWhere( (e) => e.name == json['status'], orElse: () => SyncOperationStatus.idle, ), error: json['error'], conflicts: (json['conflicts'] as List? ?? []) .map((c) => SyncConflict.fromJson(c as Map)) .toList(), statistics: json['statistics'] != null ? SyncStatistics.fromJson(json['statistics'] as Map) : const SyncStatistics(), ); } /// Create a failed sync result factory SyncResult.failure({ required String error, required SyncOperationStatus status, List conflicts = const [], }) { return SyncResult( success: false, timestamp: DateTime.now(), status: status, error: error, conflicts: conflicts, ); } /// Create a successful sync result factory SyncResult.success({ List conflicts = const [], SyncStatistics statistics = const SyncStatistics(), }) { return SyncResult( success: true, timestamp: DateTime.now(), status: conflicts.isEmpty ? SyncOperationStatus.success : SyncOperationStatus.conflictsDetected, conflicts: conflicts, statistics: statistics, ); } } /// Statistics about sync operations class SyncStatistics { final int supplementsUploaded; final int supplementsDownloaded; final int intakesUploaded; final int intakesDownloaded; final int conflictsResolved; final Duration syncDuration; const SyncStatistics({ this.supplementsUploaded = 0, this.supplementsDownloaded = 0, this.intakesUploaded = 0, this.intakesDownloaded = 0, this.conflictsResolved = 0, this.syncDuration = Duration.zero, }); Map toJson() { return { 'supplementsUploaded': supplementsUploaded, 'supplementsDownloaded': supplementsDownloaded, 'intakesUploaded': intakesUploaded, 'intakesDownloaded': intakesDownloaded, 'conflictsResolved': conflictsResolved, 'syncDurationMs': syncDuration.inMilliseconds, }; } factory SyncStatistics.fromJson(Map json) { return SyncStatistics( supplementsUploaded: json['supplementsUploaded'] ?? 0, supplementsDownloaded: json['supplementsDownloaded'] ?? 0, intakesUploaded: json['intakesUploaded'] ?? 0, intakesDownloaded: json['intakesDownloaded'] ?? 0, conflictsResolved: json['conflictsResolved'] ?? 0, syncDuration: Duration(milliseconds: json['syncDurationMs'] ?? 0), ); } int get totalUploaded => supplementsUploaded + intakesUploaded; int get totalDownloaded => supplementsDownloaded + intakesDownloaded; int get totalSynced => totalUploaded + totalDownloaded; @override String toString() { return 'SyncStatistics(uploaded: $totalUploaded, downloaded: $totalDownloaded, ' 'conflicts: $conflictsResolved, duration: ${syncDuration.inSeconds}s)'; } }