import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; import '../models/workout.dart'; import '../utils/hmac_utility.dart'; import 'database_service.dart'; /// محرك المزامنة التلقائي /// يراقب الاتصال بالإنترنت ويرسل التمارين المحفوظة محلياً عند توفر الشبكة class SyncService extends GetxService { final DatabaseService _db = Get.find(); final _storage = const FlutterSecureStorage(); 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; static const String _apiEndpointKey = 'api_endpoint'; static const String _apiKeyKey = 'api_key'; static const String _apiSecretKey = 'api_secret'; @override Future onInit() async { super.onInit(); await _checkConnectivity(); _startConnectivityMonitor(); _startPeriodicSync(); await _updatePendingCount(); } // ─── 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(); } }, ); } // ─── Credentials Management ─────────────────────────────────── Future saveCredentials({ required String apiEndpoint, required String apiKey, required String apiSecret, }) async { await _storage.write(key: _apiEndpointKey, value: apiEndpoint); await _storage.write(key: _apiKeyKey, value: apiKey); await _storage.write(key: _apiSecretKey, value: apiSecret); } Future> getCredentials() async { return { 'endpoint': await _storage.read(key: _apiEndpointKey), 'apiKey': await _storage.read(key: _apiKeyKey), 'apiSecret': await _storage.read(key: _apiSecretKey), }; } Future hasCredentials() async { final creds = await getCredentials(); return creds['apiKey'] != null && creds['apiKey']!.isNotEmpty && creds['apiSecret'] != null && creds['apiSecret']!.isNotEmpty; } // ─── Sync Operations ───────────────────────────────────────── /// Queue a workout for sync Future queueWorkout(Workout workout) async { final payload = jsonEncode(workout.toApiPayload()); await _db.enqueueSync(workout.id, payload); await _updatePendingCount(); // Try immediate sync if online if (_isOnline.value) { syncPendingWorkouts(); } } /// Process all pending sync items Future syncPendingWorkouts() async { if (_isSyncing.value) return; _isSyncing.value = true; try { final creds = await getCredentials(); if (creds['endpoint'] == null || creds['apiKey'] == null || creds['apiSecret'] == null) { debugPrint('[SYNC] No API credentials configured'); return; } final pendingItems = await _db.getPendingSyncs(); if (pendingItems.isEmpty) return; debugPrint('[SYNC] Processing ${pendingItems.length} pending items'); for (final item in pendingItems) { await _syncSingleItem( id: item['id'] as int, workoutId: item['workout_id'] as String, payload: item['payload'] as String, endpoint: creds['endpoint']!, apiKey: creds['apiKey']!, apiSecret: creds['apiSecret']!, ); } 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, required String endpoint, required String apiKey, required String apiSecret, }) async { try { final headers = HmacUtility.buildHeaders( apiKey: apiKey, apiSecret: apiSecret, payload: payload, ); final response = await http .post( Uri.parse('$endpoint/api/v1/workouts'), headers: headers, body: payload, ) .timeout(const Duration(seconds: 30)); if (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 { final items = await _db.getPendingSyncs(); _pendingCount.value = items.length; } @override void onClose() { _connectivitySub?.cancel(); _syncTimer?.cancel(); super.onClose(); } }