import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/app/data/storage/local_data_store.dart'; import 'package:relationship_saver/app/data/storage/local_data_store_hive.dart'; import 'package:relationship_saver/app/data/storage/local_data_store_provider.dart'; import 'package:relationship_saver/app/data/storage/local_data_store_shared_prefs.dart'; import 'package:relationship_saver/app/state/local_data_state.dart'; import 'package:relationship_saver/core/config/app_config.dart'; import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart'; import 'package:relationship_saver/features/dashboard/domain/dashboard_models.dart'; import 'package:relationship_saver/features/ideas/domain/idea_models.dart'; import 'package:relationship_saver/features/local/chat_preference_extractor.dart'; import 'package:relationship_saver/features/moments/domain/moment_models.dart'; import 'package:relationship_saver/features/people/domain/person_models.dart'; import 'package:relationship_saver/features/reminders/application/reminder_scheduler_provider.dart'; import 'package:relationship_saver/features/reminders/domain/reminder_models.dart'; import 'package:relationship_saver/features/share_intake/application/share_capture_models.dart'; import 'package:relationship_saver/features/share_intake/domain/share_capture_draft_suggester.dart'; import 'package:relationship_saver/features/share_intake/domain/share_models.dart'; import 'package:relationship_saver/features/sync/data/sync_queue_repository.dart'; import 'package:relationship_saver/integrations/backend/models/backend_models.dart'; import 'package:uuid/uuid.dart'; part 'relationship_repository_activity.dart'; part 'relationship_repository_ai_digest.dart'; part 'relationship_repository_people.dart'; part 'relationship_repository_share.dart'; part 'relationship_repository_sync.dart'; const int _schemaVersion = 5; const int _syncSchemaVersion = 1; const Uuid _uuid = Uuid(); const ChatPreferenceExtractor _chatPreferenceExtractor = ChatPreferenceExtractor(); /// Persisted local-first app store used by the current prototype. /// /// The storage backend is still a single local record because that keeps the /// prototype stable, but the repository implementation is now split by feature /// behavior so it reads like vertical slices instead of one monolithic file. class LocalRepository extends AsyncNotifier { @override Future build() async { final LocalDataStore store = ref.read(localDataStoreProvider); LocalDataRecord? record = await store.read(); if ((record == null || record.rawState.isEmpty) && AppConfig.useHiveLocalDb && store is HiveLocalDataStore) { final SharedPrefsLocalDataStore legacyStore = SharedPrefsLocalDataStore(); final LocalDataRecord? legacy = await legacyStore.read(); if (legacy != null && legacy.rawState.isNotEmpty) { await store.write( rawState: legacy.rawState, schemaVersion: legacy.schemaVersion, ); await legacyStore.clear(); record = await store.read(); } } if (record == null || record.rawState.isEmpty) { final LocalDataState seeded = LocalDataState.seed(); await _persist(seeded); await _reconcileReminderSchedule(seeded); return seeded; } final LocalDataRecord migrated = await _migrateIfNeeded(record); try { final Map json = jsonDecode(migrated.rawState) as Map; final LocalDataState loaded = LocalDataState.fromJson(json); await _reconcileReminderSchedule(loaded); return loaded; } on FormatException { final LocalDataState fallback = LocalDataState.seed(); await _persist(fallback); await _reconcileReminderSchedule(fallback); return fallback; } } Future _setState(LocalDataState next) async { state = AsyncData(next); await _persist(next); await _reconcileReminderSchedule(next); } LocalDataState _requireState() { final LocalDataState? value = state.asData?.value; if (value == null) { throw StateError('Local data state is not ready yet'); } return value; } Future _persist(LocalDataState state) async { await ref .read(localDataStoreProvider) .write( rawState: jsonEncode(state.toJson()), schemaVersion: _schemaVersion, ); } Future _reconcileReminderSchedule(LocalDataState state) async { try { await ref.read(reminderSchedulerProvider).reconcile(state.reminders); } catch (_) { // Scheduling failures must not block local-first data updates. } } Future _migrateIfNeeded(LocalDataRecord record) async { final int currentVersion = record.schemaVersion; if (currentVersion == _schemaVersion) { return record; } final LocalDataStore store = ref.read(localDataStoreProvider); if (currentVersion <= 0) { final LocalDataState seeded = LocalDataState.seed(); await store.clear(); await _persist(seeded); return LocalDataRecord( rawState: jsonEncode(seeded.toJson()), schemaVersion: _schemaVersion, ); } try { final Map json = jsonDecode(record.rawState) as Map; final LocalDataState migratedState = LocalDataState.fromJson(json); final String migratedRawState = jsonEncode(migratedState.toJson()); await store.write( rawState: migratedRawState, schemaVersion: _schemaVersion, ); return LocalDataRecord( rawState: migratedRawState, schemaVersion: _schemaVersion, ); } on FormatException { final LocalDataState seeded = LocalDataState.seed(); await store.clear(); await _persist(seeded); return LocalDataRecord( rawState: jsonEncode(seeded.toJson()), schemaVersion: _schemaVersion, ); } } Future _enqueueChange(ChangeEnvelope change) async { await ref.read(syncQueueRepositoryProvider.notifier).enqueue(change); } Future _enqueueChanges(List changes) async { await ref.read(syncQueueRepositoryProvider.notifier).enqueueAll(changes); } } String? _trimToNull(String? value) { if (value == null) { return null; } final String trimmed = value.trim(); return trimmed.isEmpty ? null : trimmed; } final AsyncNotifierProvider localRepositoryProvider = AsyncNotifierProvider(LocalRepository.new);