import 'package:hive_flutter/hive_flutter.dart'; import 'package:intl/intl.dart'; import '../local/models.dart'; import 'myquran_service.dart'; class ScheduleCacheStatus { final DateTime? startDate; final DateTime? endDate; final int cachedDays; final int daysUntilRefresh; const ScheduleCacheStatus({ required this.startDate, required this.endDate, required this.cachedDays, required this.daysUntilRefresh, }); const ScheduleCacheStatus.empty() : startDate = null, endDate = null, cachedDays = 0, daysUntilRefresh = -1; bool get hasData => startDate != null && endDate != null && cachedDays > 0; bool get isExpired => hasData && daysUntilRefresh < 0; bool get needsRefreshSoon => hasData && daysUntilRefresh <= 3; static ScheduleCacheStatus fromSchedules( Iterable schedules, DateTime referenceDate, ) { DateTime? startDate; DateTime? endDate; var cachedDays = 0; for (final schedule in schedules) { final parsedDate = DateTime.tryParse(schedule.date); if (parsedDate == null) continue; final normalized = DateTime( parsedDate.year, parsedDate.month, parsedDate.day, ); cachedDays++; startDate = startDate == null || normalized.isBefore(startDate) ? normalized : startDate; endDate = endDate == null || normalized.isAfter(endDate) ? normalized : endDate; } if (startDate == null || endDate == null || cachedDays == 0) { return const ScheduleCacheStatus.empty(); } final today = DateTime( referenceDate.year, referenceDate.month, referenceDate.day, ); return ScheduleCacheStatus( startDate: startDate, endDate: endDate, cachedDays: cachedDays, daysUntilRefresh: endDate.difference(today).inDays, ); } } /// Service to sync monthly prayer data from MyQuran API → Hive. class SyncService { SyncService._(); static final SyncService instance = SyncService._(); static const Duration _autoRefreshCooldown = Duration(hours: 12); static Set rollingWindowMonths(DateTime referenceDate) { final currentMonth = DateTime(referenceDate.year, referenceDate.month, 1); final nextMonth = DateTime(referenceDate.year, referenceDate.month + 1, 1); return { DateFormat('yyyy-MM').format(currentMonth), DateFormat('yyyy-MM').format(nextMonth), }; } static List staleScheduleKeys( Iterable keys, DateTime referenceDate, ) { final allowedMonths = rollingWindowMonths(referenceDate); final staleKeys = []; for (final key in keys) { final parsed = DateTime.tryParse(key); if (parsed == null) { staleKeys.add(key); continue; } final monthKey = DateFormat('yyyy-MM').format(parsed); if (!allowedMonths.contains(monthKey)) { staleKeys.add(key); } } return staleKeys; } Future _pruneScheduleCache( Box scheduleBox, DateTime referenceDate, ) async { final staleKeys = staleScheduleKeys( scheduleBox.keys.cast(), referenceDate, ); if (staleKeys.isNotEmpty) { await scheduleBox.deleteAll(staleKeys); await scheduleBox.compact(); } } bool _shouldAttemptAutoRefresh({ required ScheduleCacheStatus status, required bool hasTodayData, }) { return !hasTodayData || !status.hasData || status.needsRefreshSoon; } DateTime? _parseAttemptTimestamp(String? value) { if (value == null || value.isEmpty) return null; return DateTime.tryParse(value); } /// Sync current month + next month prayer data for the configured city. /// Returns true on success. Future syncMonthlyData({DateTime? referenceDate}) async { final settingsBox = Hive.box(HiveBoxes.settings); final settings = settingsBox.get('default'); if (settings == null) return false; final cityId = settings.cityIdApi; final now = referenceDate ?? DateTime.now(); final currentMonth = DateFormat('yyyy-MM').format(now); // Also fetch next month for continuity final nextMonthDate = DateTime(now.year, now.month + 1, 1); final nextMonth = DateFormat('yyyy-MM').format(nextMonthDate); final api = MyQuranSholatService.instance; final scheduleBox = Hive.box(HiveBoxes.prayerSchedule); var success = false; var hasCurrentMonth = false; var hasNextMonth = false; // Fetch current month final currentData = await api.getMonthlySchedule(cityId, currentMonth); if (currentData.isNotEmpty) { hasCurrentMonth = true; for (final entry in currentData.entries) { final jadwal = entry.value; scheduleBox.put( entry.key, DailyPrayerSchedule( date: entry.key, imsak: jadwal['imsak'] ?? '00:00', subuh: jadwal['subuh'] ?? '00:00', terbit: jadwal['terbit'] ?? '00:00', dhuha: jadwal['dhuha'] ?? '00:00', dzuhur: jadwal['dzuhur'] ?? '00:00', ashar: jadwal['ashar'] ?? '00:00', maghrib: jadwal['maghrib'] ?? '00:00', isya: jadwal['isya'] ?? '00:00', ), ); } success = true; } // Fetch next month final nextData = await api.getMonthlySchedule(cityId, nextMonth); if (nextData.isNotEmpty) { hasNextMonth = true; for (final entry in nextData.entries) { final jadwal = entry.value; scheduleBox.put( entry.key, DailyPrayerSchedule( date: entry.key, imsak: jadwal['imsak'] ?? '00:00', subuh: jadwal['subuh'] ?? '00:00', terbit: jadwal['terbit'] ?? '00:00', dhuha: jadwal['dhuha'] ?? '00:00', dzuhur: jadwal['dzuhur'] ?? '00:00', ashar: jadwal['ashar'] ?? '00:00', maghrib: jadwal['maghrib'] ?? '00:00', isya: jadwal['isya'] ?? '00:00', ), ); } } if (success) { if (hasCurrentMonth && hasNextMonth) { await _pruneScheduleCache(scheduleBox, now); } settings.lastSyncDate = DateFormat('yyyy-MM-dd HH:mm').format(now); await settings.save(); } return success; } Future autoRefreshIfNeeded({ DateTime? referenceDate, }) async { final settingsBox = Hive.box(HiveBoxes.settings); final settings = settingsBox.get('default'); if (settings == null) { return const AutoRefreshResult.skipped('settings-missing'); } final now = referenceDate ?? DateTime.now(); final status = getCacheStatus(now); final hasTodayData = getTodaySchedule(now) != null; if (!_shouldAttemptAutoRefresh(status: status, hasTodayData: hasTodayData)) { return const AutoRefreshResult.skipped('cache-fresh'); } final lastAttempt = _parseAttemptTimestamp(settings.lastAutoSyncAttemptDate); if (lastAttempt != null && now.difference(lastAttempt) < _autoRefreshCooldown) { return const AutoRefreshResult.skipped('cooldown'); } settings.lastAutoSyncAttemptDate = now.toIso8601String(); await settings.save(); final synced = await syncMonthlyData(referenceDate: now); return synced ? const AutoRefreshResult.synced() : const AutoRefreshResult.failed('sync-failed'); } /// Get today's prayer schedule from local Hive cache. DailyPrayerSchedule? getTodaySchedule([DateTime? targetDate]) { final scheduleBox = Hive.box(HiveBoxes.prayerSchedule); final dateToFetch = targetDate ?? DateTime.now(); final dateStr = DateFormat('yyyy-MM-dd').format(dateToFetch); return scheduleBox.get(dateStr); } ScheduleCacheStatus getCacheStatus([DateTime? referenceDate]) { final scheduleBox = Hive.box(HiveBoxes.prayerSchedule); return ScheduleCacheStatus.fromSchedules( scheduleBox.values, referenceDate ?? DateTime.now(), ); } } class AutoRefreshResult { final bool attempted; final bool synced; final String reason; const AutoRefreshResult._({ required this.attempted, required this.synced, required this.reason, }); const AutoRefreshResult.skipped(String reason) : this._(attempted: false, synced: false, reason: reason); const AutoRefreshResult.synced() : this._(attempted: true, synced: true, reason: 'synced'); const AutoRefreshResult.failed(String reason) : this._(attempted: true, synced: false, reason: reason); }