import 'package:hive_flutter/hive_flutter.dart'; import 'package:intl/intl.dart'; import '../local/hive_boxes.dart'; import '../local/models/app_settings.dart'; /// Runtime persistence for notification counters and cursors. class NotificationRuntimeService { NotificationRuntimeService._(); static final NotificationRuntimeService instance = NotificationRuntimeService._(); static const _nonPrayerCountPrefix = 'non_prayer_push_count.'; static const _lastRemoteSyncKey = 'remote.last_sync_at'; static const _lastWeeklySummaryKey = 'summary.last_week_key'; Box get _box => Hive.box(HiveBoxes.notificationRuntime); String _todayKey() => DateFormat('yyyy-MM-dd').format(DateTime.now()); int nonPrayerPushCountToday() { return (_box.get('$_nonPrayerCountPrefix${_todayKey()}') as int?) ?? 0; } Future incrementNonPrayerPushCount() async { final key = '$_nonPrayerCountPrefix${_todayKey()}'; final next = ((_box.get(key) as int?) ?? 0) + 1; await _box.put(key, next); } bool isWithinQuietHours(AppSettings settings, {DateTime? now}) { final current = now ?? DateTime.now(); final startParts = _parseHourMinute(settings.quietHoursStart); final endParts = _parseHourMinute(settings.quietHoursEnd); if (startParts == null || endParts == null) return false; final currentMinutes = current.hour * 60 + current.minute; final startMinutes = startParts.$1 * 60 + startParts.$2; final endMinutes = endParts.$1 * 60 + endParts.$2; if (startMinutes == endMinutes) { // Same value means quiet-hours disabled. return false; } if (startMinutes < endMinutes) { return currentMinutes >= startMinutes && currentMinutes < endMinutes; } // Overnight interval (e.g. 22:00 -> 05:00). return currentMinutes >= startMinutes || currentMinutes < endMinutes; } bool canSendNonPrayerPush(AppSettings settings, {DateTime? now}) { if (!settings.alertsEnabled) return false; if (isWithinQuietHours(settings, now: now)) return false; return nonPrayerPushCountToday() < settings.maxNonPrayerPushPerDay; } DateTime? lastRemoteSyncAt() { final raw = (_box.get(_lastRemoteSyncKey) ?? '').toString(); if (raw.isEmpty) return null; return DateTime.tryParse(raw); } Future setLastRemoteSyncAt(DateTime value) async { await _box.put(_lastRemoteSyncKey, value.toIso8601String()); } String? lastWeeklySummaryWeekKey() { final raw = (_box.get(_lastWeeklySummaryKey) ?? '').toString(); return raw.isEmpty ? null : raw; } Future setLastWeeklySummaryWeekKey(String key) async { await _box.put(_lastWeeklySummaryKey, key); } (int, int)? _parseHourMinute(String value) { final match = RegExp(r'^(\d{1,2}):(\d{2})$').firstMatch(value.trim()); if (match == null) return null; final hour = int.tryParse(match.group(1) ?? ''); final minute = int.tryParse(match.group(2) ?? ''); if (hour == null || minute == null) return null; if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null; return (hour, minute); } }