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 '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}) async { if (_isSyncing.value) return; _isSyncing.value = true; try { if (!_auth.isSignedIn) { debugPrint('[SYNC] Sign in to sync saved workouts'); return; } final pendingItems = await _db.getPendingSyncs( includeExhausted: retryNow, ); if (pendingItems.isEmpty) return; 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(); } catch (e) { debugPrint('[SYNC] Error during sync: $e'); } finally { _isSyncing.value = false; } } 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(); } 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(); } }