library; import 'dart:async'; import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:smart_attendance_app/core/constants.dart'; import 'package:smart_attendance_app/core/attendance_constants.dart'; import 'package:smart_attendance_app/data/api/dio_client.dart'; import 'package:smart_attendance_app/data/api/student_api.dart'; import 'package:smart_attendance_app/domain/models/offline_payload.dart'; import 'package:smart_attendance_app/data/local/hive_service.dart'; import 'package:smart_attendance_app/data/local/notification_service.dart'; import 'package:smart_attendance_app/data/local/pending_count_provider.dart'; import 'package:smart_attendance_app/utils/logger.dart'; final offlineSyncServiceProvider = Provider((ref) { return OfflineSyncService( ref: ref, studentApi: ref.read(studentApiProvider), hive: ref.read(hiveServiceProvider), notificationService: ref.read(notificationServiceProvider), notificationsNotifier: ref.read(notificationsProvider.notifier), dio: ref.read(dioProvider), ); }); class OfflineSyncService { final Ref _ref; final StudentApi _studentApi; final HiveService _hive; final NotificationService _notificationService; final NotificationsNotifier _notificationsNotifier; final Dio _dio; StreamSubscription>? _connectivitySub; bool _isSyncing = false; OfflineSyncService({ required Ref ref, required StudentApi studentApi, required HiveService hive, required NotificationService notificationService, required NotificationsNotifier notificationsNotifier, required Dio dio, }) : _ref = ref, _studentApi = studentApi, _hive = hive, _notificationService = notificationService, _notificationsNotifier = notificationsNotifier, _dio = dio; void startListening() { _connectivitySub = Connectivity() .onConnectivityChanged .listen((List results) { final isOnline = results.any((r) => r != ConnectivityResult.none); if (isOnline) { syncQueue(); } }); } String _className(OfflineAttendancePayload p) => p.className ?? 'a class'; Future _notify(String title, String body, String severity) => _notificationService.addNotification(title: title, body: body, severity: severity); Future _removePayload(dynamic payload) async { final key = payload.key; if (key != null && key is int) { await _hive.removeFromQueue(key); _ref.read(pendingCountProvider.notifier).refresh(); } } Future _deleteImageFile(String imagePath) async { try { final file = File(imagePath); if (await file.exists()) await file.delete(); } catch (_) {} } Future syncQueue() async { if (_isSyncing) return 0; final queue = _hive.getQueue(); if (queue.isEmpty) return 0; try { final response = await _dio.get>('/health'); if (response.statusCode != 200 || response.data?['status'] != 'healthy') return 0; } catch (_) { return 0; } _isSyncing = true; int syncedCount = 0; try { for (final payload in queue) { if (await _processPayload(payload)) syncedCount++; } } finally { _isSyncing = false; if (syncedCount > 0) await _notificationsNotifier.load(); } return syncedCount; } Future _processPayload(OfflineAttendancePayload payload) async { final age = DateTime.now().difference(payload.capturedAt); if (age > kOfflinePayloadMaxAge) { await _removePayload(payload); await _deleteImageFile(payload.imagePath); await _notify('Submission Expired', 'Attendance for ${_className(payload)} was captured ${age.inMinutes} minutes ago and has expired.', kSeverityWarning); return false; } try { await _studentApi.markAttendance( sessionId: payload.sessionId, latitude: payload.latitude, longitude: payload.longitude, accuracy: payload.accuracy ?? 0.0, imagePath: payload.imagePath, ); await _removePayload(payload); await _deleteImageFile(payload.imagePath); await _notify('Offline Sync Complete', 'Successfully synced offline attendance for ${_className(payload)}.', kSeveritySuccess); return true; } on DioException catch (e) { final statusCode = e.response?.statusCode ?? 0; if (statusCode >= 400 && statusCode < 500) { await _removePayload(payload); await _deleteImageFile(payload.imagePath); await _notify('Submission Rejected', 'Attendance for ${_className(payload)} was rejected by the server ($statusCode).', kSeverityDanger); return false; } _handleSyncError(payload); return false; } catch (e) { AppLogger.error('Sync failed: $e'); _handleSyncError(payload); return false; } } Future _handleSyncError(OfflineAttendancePayload payload) async { AppLogger.error('Sync failed for ${_className(payload)}'); await _notify('Sync Failed', 'Could not sync attendance for ${_className(payload)}. Will retry automatically.', kSeverityWarning); } void dispose() { _connectivitySub?.cancel(); } }