import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import '../models/workout.dart'; import '../models/meal_entry.dart'; import 'auth_service.dart'; import 'database_service.dart'; /// محرك المزامنة التلقائي /// يراقب الاتصال بالإنترنت ويرسل التمارين المحفوظة محلياً عند توفر الشبكة class SyncService extends GetxService { final DatabaseService _db = Get.find(); final AuthService _auth = Get.find(); final _connectivity = Connectivity(); StreamSubscription? _connectivitySub; Timer? _syncTimer; final _isSyncing = false.obs; final _isOnline = false.obs; final _pendingCount = 0.obs; bool get isSyncing => _isSyncing.value; bool get isOnline => _isOnline.value; int get pendingCount => _pendingCount.value; Future init() async { await _checkConnectivity(); _startConnectivityMonitor(); _startPeriodicSync(); await _updatePendingCount(); return this; } // ─── Connectivity Monitoring ────────────────────────────────── Future _checkConnectivity() async { final results = await _connectivity.checkConnectivity(); _isOnline.value = !results.contains(ConnectivityResult.none); } void _startConnectivityMonitor() { _connectivitySub = _connectivity.onConnectivityChanged.listen( (List results) { final wasOffline = !_isOnline.value; _isOnline.value = !results.contains(ConnectivityResult.none); // Came back online → trigger sync if (wasOffline && _isOnline.value) { debugPrint('[SYNC] Back online — triggering sync'); syncPendingWorkouts(); } }, ); } void _startPeriodicSync() { // Check for pending syncs every 2 minutes _syncTimer = Timer.periodic( const Duration(minutes: 2), (_) { if (_isOnline.value && !_isSyncing.value) { syncPendingWorkouts(); } }, ); } // ─── Sync Operations ───────────────────────────────────────── /// Queue a workout for sync Future queueWorkout(Workout workout) async { final payload = jsonEncode(workout.toApiPayload()); final queueForSync = workout.coordinates.length >= 2 && workout.routePolyline != null && workout.routePolyline!.isNotEmpty; await _db.saveCompletedWorkout(workout, payload, queueForSync: queueForSync); await _updatePendingCount(); // Try immediate sync if online if (queueForSync && _isOnline.value) { syncPendingWorkouts(); } } Future retryWorkout(String workoutId) async { await _db.retryFailedSync(workoutId); await _updatePendingCount(); if (_isOnline.value && !_isSyncing.value) { await syncPendingWorkouts(); } } /// Process all pending sync items Future syncPendingWorkouts({ bool retryNow = false, bool pullMeals = false, }) async { if (_isSyncing.value) return; _isSyncing.value = true; try { if (!_auth.isSignedIn) { debugPrint('[SYNC] Sign in to sync saved workouts'); return; } await _syncPendingMeals(); final pendingItems = await _db.getPendingSyncs( includeExhausted: retryNow, ); if (pendingItems.isNotEmpty) { debugPrint('[SYNC] Processing ${pendingItems.length} pending items'); for (final item in pendingItems) { if (!retryNow && !_isRetryDue(item)) continue; await _syncSingleItem( id: item['id'] as int, workoutId: item['workout_id'] as String, payload: item['payload'] as String, ); } await _db.removeSentFromQueue(); } await _updatePendingCount(); if (pullMeals) { try { await _pullRecentMeals(); } catch (error) { debugPrint('[SYNC] Meal history refresh failed: $error'); } } } catch (e) { debugPrint('[SYNC] Error during sync: $e'); } finally { _isSyncing.value = false; } } Future queueMeal(MealEntry meal) async { meal.syncStatus = MealSyncStatus.pending; await _db.saveMeal(meal); await _updatePendingCount(); if (_isOnline.value && _auth.isSignedIn && !_isSyncing.value) { await syncPendingWorkouts(); } } Future syncMealsNow() async { if (!_isOnline.value || !_auth.isSignedIn || _isSyncing.value) return; await syncPendingWorkouts(retryNow: true, pullMeals: true); } Future _syncPendingMeals() async { final pendingMeals = await _db.getPendingMeals(); for (final meal in pendingMeals) { try { final response = await _auth.authenticatedPost( Uri.parse('${_auth.apiBase}/meals.php'), jsonEncode(meal.toApiPayload()), ); if (response.statusCode == 200 || response.statusCode == 201) { await _db.updateMealSyncStatus(meal.id, MealSyncStatus.synced); } else { await _db.updateMealSyncStatus( meal.id, MealSyncStatus.failed, error: 'HTTP ${response.statusCode}', ); } } catch (error) { await _db.updateMealSyncStatus( meal.id, MealSyncStatus.failed, error: error.toString(), ); } } } Future _pullRecentMeals() async { final now = DateTime.now().toUtc(); final from = now.subtract(const Duration(days: 30)); final to = now.add(const Duration(days: 1)); final uri = Uri.parse('${_auth.apiBase}/meals.php').replace( queryParameters: { 'from': '${from.year.toString().padLeft(4, '0')}-${from.month.toString().padLeft(2, '0')}-${from.day.toString().padLeft(2, '0')}', 'to': '${to.year.toString().padLeft(4, '0')}-${to.month.toString().padLeft(2, '0')}-${to.day.toString().padLeft(2, '0')}', }, ); final response = await _auth.authenticatedGet(uri); if (response.statusCode != 200) { throw AuthException('تعذر جلب سجل الطعام (${response.statusCode}).'); } final decoded = jsonDecode(response.body); if (decoded is! Map || decoded['meals'] is! List) return; for (final raw in decoded['meals'] as List) { if (raw is! Map) continue; final local = { 'id': raw['client_meal_uuid'], 'name': raw['name'], 'meal_type': raw['meal_type'], 'calories': raw['calories'], 'protein_grams': raw['protein_grams'], 'carbohydrate_grams': raw['carbohydrate_grams'], 'fat_grams': raw['fat_grams'], 'consumed_at': raw['consumed_at'], 'source': raw['source'], 'notes': raw['notes'], 'sync_status': MealSyncStatus.synced.name, }; await _db.mergeSyncedMeal(MealEntry.fromMap(local)); } } Future _syncSingleItem({ required int id, required String workoutId, required String payload, }) async { try { final response = await _auth.authenticatedPost( Uri.parse('${_auth.apiBase}/workouts.php'), payload, ); if (response.statusCode == 200 || response.statusCode == 201) { await _db.updateSyncQueueItem(id, 'sent'); await _db.updateSyncStatus( workoutId, SyncStatus.synced, syncedAt: DateTime.now(), ); debugPrint('[SYNC] ✅ Workout $workoutId synced'); } else { await _db.updateSyncQueueItem( id, 'failed', errorMessage: 'HTTP ${response.statusCode}: ${response.body}', ); debugPrint('[SYNC] ❌ Failed: ${response.statusCode}'); } } catch (e) { await _db.updateSyncQueueItem(id, 'failed', errorMessage: e.toString()); debugPrint('[SYNC] ❌ Error: $e'); } } Future _updatePendingCount() async { _pendingCount.value = await _db.getPendingSyncCount() + await _db.getPendingMealCount(); } bool _isRetryDue(Map item) { if (item['status'] != 'failed') return true; final lastAttempt = item['last_attempt'] as String?; if (lastAttempt == null) return true; final retryCount = item['retry_count'] as int? ?? 0; final delaySeconds = (30 * (1 << retryCount)).clamp(30, 3600); final attemptedAt = DateTime.tryParse(lastAttempt); if (attemptedAt == null) return true; return DateTime.now().toUtc().difference(attemptedAt.toUtc()).inSeconds >= delaySeconds; } @override void onClose() { _connectivitySub?.cancel(); _syncTimer?.cancel(); super.onClose(); } }