import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/core/config/app_config.dart'; import 'package:relationship_saver/features/local/local_models.dart'; import 'package:relationship_saver/features/local/local_repository.dart'; import 'package:relationship_saver/features/signals/domain/signals_feed_synthesizer.dart'; import 'package:relationship_saver/integrations/backend/backend_gateway_provider.dart'; import 'package:relationship_saver/integrations/backend/models/backend_models.dart'; class SignalsController extends AsyncNotifier { final Set _hiddenSignalIds = {}; @override Future build() async { final LocalDataState localData = await ref.watch( localRepositoryProvider.future, ); return _load(localData: localData); } Future refresh() async { state = const AsyncLoading(); try { final LocalDataState localData = await ref.read( localRepositoryProvider.future, ); final SignalsFeed feed = await _load(localData: localData); state = AsyncData(feed); } catch (error, stackTrace) { state = AsyncError(error, stackTrace); } } Future acknowledge(String id, SignalAction action) async { final SignalsFeed? current = state.asData?.value; if (current == null) { return; } final SignalItem? item = current.items .where((SignalItem value) => value.id == id) .cast() .firstOrNull; if (item == null) { return; } if (_isLocalSignal(item)) { if (action == SignalAction.saved) { await _materializeLocalSignal(item); } await ref.read(localRepositoryProvider.notifier).dismissLocalSignal(id); } else { await ref .read(backendGatewayProvider) .acknowledgeSignal(signalId: id, action: action); _hiddenSignalIds.add(id); } final List updated = current.items .where((SignalItem item) => item.id != id) .toList(growable: false); state = AsyncData(current.copyWith(items: updated)); } Future _load({required LocalDataState localData}) async { final List people = localData.people; final List items = [ ...SignalsFeedSynthesizer.synthesize(state: localData), ]; String? cursor; if (!AppConfig.useFakeBackend || items.isEmpty) { try { final SignalsFeed remote = await ref .read(backendGatewayProvider) .getSignalsFeed(limit: 20); cursor = remote.cursor; items.addAll(remote.items); } catch (_) { if (items.isEmpty) { items.addAll(_fallbackSignals(people)); cursor = 'fallback-signals'; } } } final List visible = items .where((SignalItem item) => !_hiddenSignalIds.contains(item.id)) .toList(growable: false); return SignalsFeed(cursor: cursor, items: visible); } bool _isLocalSignal(SignalItem item) { return item.metadata?[signalMetadataSourceKey] == localSignalSource; } Future _materializeLocalSignal(SignalItem item) async { final Map? metadata = item.metadata; if (metadata == null) { return; } if (metadata[signalMetadataActionKey] != createReminderSignalAction) { return; } final String? title = metadata[signalMetadataReminderTitleKey] as String?; final String? cadenceName = metadata[signalMetadataReminderCadenceKey] as String?; final String? nextAtRaw = metadata[signalMetadataReminderNextAtKey] as String?; if (title == null || cadenceName == null || nextAtRaw == null) { return; } final LocalDataState localData = await ref.read( localRepositoryProvider.future, ); final bool duplicate = localData.reminders.any((ReminderRule reminder) { return reminder.personId == item.personId && reminder.title.trim().toLowerCase() == title.trim().toLowerCase(); }); if (duplicate) { return; } await ref .read(localRepositoryProvider.notifier) .addReminder( title: title, cadence: ReminderCadence.values.firstWhere( (ReminderCadence value) => value.name == cadenceName, orElse: () => ReminderCadence.weekly, ), nextAt: DateTime.parse(nextAtRaw).toLocal(), personId: item.personId, ); } List _fallbackSignals(List people) { final PersonProfile defaultPerson = people.isNotEmpty ? people.first : PersonProfile( id: 'person-default', name: 'Someone Important', relationship: 'Relationship', affinityScore: 70, nextMoment: DateTime(2026, 2, 20), tags: [], notes: '', ); final PersonProfile secondaryPerson = people.length > 1 ? people[1] : defaultPerson; final DateTime now = DateTime.now().toUtc(); return [ SignalItem( id: 'fallback-1', type: 'recommendation', title: 'Check in with ${defaultPerson.name} tonight', description: 'A short voice memo can keep momentum.', personId: defaultPerson.id, createdAt: now, ), SignalItem( id: 'fallback-2', type: 'trend', title: 'Gift idea: practical + personal is trending', description: 'Combine utility with one sentimental detail.', personId: secondaryPerson.id, createdAt: now.subtract(const Duration(hours: 2)), ), ]; } } final AsyncNotifierProvider signalsControllerProvider = AsyncNotifierProvider( SignalsController.new, ); extension on Iterable { T? get firstOrNull => isEmpty ? null : first; }