import 'dart:convert'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; import '../local/models/app_settings.dart'; import 'notification_inbox_service.dart'; import 'notification_runtime_service.dart'; import 'notification_service.dart'; /// Pulls server-defined notification content and maps it to local inbox items. class RemoteNotificationContentService { RemoteNotificationContentService._(); static final RemoteNotificationContentService instance = RemoteNotificationContentService._(); final NotificationInboxService _inbox = NotificationInboxService.instance; final NotificationRuntimeService _runtime = NotificationRuntimeService.instance; Future sync({ required AppSettings settings, }) async { if (!settings.inboxEnabled) return; final endpoint = (dotenv.env['NOTIFICATION_FEED_URL'] ?? '').trim(); if (endpoint.isEmpty) return; final now = DateTime.now(); final lastSync = _runtime.lastRemoteSyncAt(); if (lastSync != null && now.difference(lastSync) < const Duration(hours: 6)) { return; } try { final response = await http.get(Uri.parse(endpoint)); if (response.statusCode < 200 || response.statusCode >= 300) return; final decoded = json.decode(response.body); final items = _extractItems(decoded); if (items.isEmpty) return; for (final raw in items) { final id = (raw['id'] ?? '').toString().trim(); final title = (raw['title'] ?? '').toString().trim(); final body = (raw['body'] ?? '').toString().trim(); if (id.isEmpty || title.isEmpty || body.isEmpty) continue; final deeplink = (raw['deeplink'] ?? '').toString().trim(); final type = (raw['type'] ?? 'content').toString().trim(); final expiresAt = DateTime.tryParse((raw['expiresAt'] ?? '').toString().trim()); final isPinned = raw['isPinned'] == true; final shouldPush = raw['push'] == true; await _inbox.addItem( title: title, body: body, type: type.isEmpty ? 'content' : type, source: 'remote', deeplink: deeplink.isEmpty ? null : deeplink, dedupeKey: 'remote.$id', expiresAt: expiresAt, isPinned: isPinned, meta: {'remoteId': id}, ); if (shouldPush && settings.alertsEnabled) { final notif = NotificationService.instance; await notif.showNonPrayerAlert( settings: settings, id: notif.nonPrayerNotificationId('remote.push.$id'), title: title, body: body, payloadType: 'content', silent: true, ); } } await _runtime.setLastRemoteSyncAt(now); } catch (_) { // Non-fatal: remote feed is optional. } } List> _extractItems(dynamic decoded) { if (decoded is List) { return decoded.whereType().map(_toStringKeyedMap).toList(); } if (decoded is Map) { final list = decoded['items']; if (list is List) { return list.whereType().map(_toStringKeyedMap).toList(); } } return const >[]; } Map _toStringKeyedMap(Map raw) { return raw.map((key, value) => MapEntry(key.toString(), value)); } }