import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/features/local/local_models.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; /// Persisted local data source for offline-first product state. class LocalRepository extends AsyncNotifier { static const String _storageKey = 'local_data_state_v1'; static const String _schemaVersionKey = 'local_data_schema_version'; static const int _schemaVersion = 2; static const Uuid _uuid = Uuid(); @override Future build() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); await _migrateIfNeeded(prefs); final String? raw = prefs.getString(_storageKey); if (raw == null || raw.isEmpty) { final LocalDataState seeded = LocalDataState.seed(); await _persist(seeded); return seeded; } try { final Map json = jsonDecode(raw) as Map; return LocalDataState.fromJson(json); } on FormatException { final LocalDataState fallback = LocalDataState.seed(); await _persist(fallback); return fallback; } } Future addPerson({ required String name, required String relationship, required String notes, required List tags, DateTime? nextMoment, }) async { final String normalizedName = name.trim(); final String normalizedRelationship = relationship.trim(); if (normalizedName.isEmpty || normalizedRelationship.isEmpty) { throw ArgumentError('Name and relationship are required'); } final LocalDataState current = _requireState(); final PersonProfile person = PersonProfile( id: 'p-${_uuid.v4()}', name: normalizedName, relationship: normalizedRelationship, affinityScore: 75, nextMoment: nextMoment ?? DateTime.now().add(const Duration(days: 3)), tags: tags, notes: notes.trim(), ); final List people = [ person, ...current.people, ]; await _setState(current.copyWith(people: people)); } Future updatePerson(PersonProfile person) async { if (person.name.trim().isEmpty || person.relationship.trim().isEmpty) { throw ArgumentError('Name and relationship are required'); } final LocalDataState current = _requireState(); final List updated = current.people .map((PersonProfile item) => item.id == person.id ? person : item) .toList(growable: false); await _setState(current.copyWith(people: updated)); } Future deletePerson(String personId) async { final LocalDataState current = _requireState(); final List people = current.people .where((PersonProfile person) => person.id != personId) .toList(growable: false); final List moments = current.moments .where((RelationshipMoment moment) => moment.personId != personId) .toList(growable: false); final List ideas = current.ideas .where((RelationshipIdea idea) => idea.personId != personId) .toList(growable: false); final List reminders = current.reminders .where((ReminderRule reminder) => reminder.personId != personId) .toList(growable: false); await _setState( current.copyWith( people: people, moments: moments, ideas: ideas, reminders: reminders, ), ); } Future addMoment({ required String personId, required String summary, String type = 'capture', }) async { final LocalDataState current = _requireState(); final String normalized = summary.trim(); if (normalized.isEmpty) { throw ArgumentError('Capture summary cannot be empty'); } final String payload = normalized.length > 280 ? normalized.substring(0, 280) : normalized; final String title = _titleFromSummary(payload); final RelationshipMoment moment = RelationshipMoment( id: 'm-${_uuid.v4()}', personId: personId, title: title, summary: payload, at: DateTime.now(), type: type, ); final List moments = [ moment, ...current.moments, ]; await _setState(current.copyWith(moments: moments)); } Future updateMoment(RelationshipMoment moment) async { if (moment.summary.trim().isEmpty) { throw ArgumentError('Capture summary cannot be empty'); } final LocalDataState current = _requireState(); final List updated = current.moments .map((RelationshipMoment item) => item.id == moment.id ? moment : item) .toList(growable: false); await _setState(current.copyWith(moments: updated)); } Future deleteMoment(String momentId) async { final LocalDataState current = _requireState(); final List moments = current.moments .where((RelationshipMoment moment) => moment.id != momentId) .toList(growable: false); await _setState(current.copyWith(moments: moments)); } Future addIdea({ required IdeaType type, required String title, required String details, String? personId, }) async { final String normalizedTitle = title.trim(); if (normalizedTitle.isEmpty) { throw ArgumentError('Idea title is required'); } final LocalDataState current = _requireState(); final RelationshipIdea idea = RelationshipIdea( id: 'i-${_uuid.v4()}', personId: personId, type: type, title: normalizedTitle, details: details.trim(), createdAt: DateTime.now(), ); final List ideas = [ idea, ...current.ideas, ]; await _setState(current.copyWith(ideas: ideas)); } Future updateIdea(RelationshipIdea idea) async { if (idea.title.trim().isEmpty) { throw ArgumentError('Idea title is required'); } final LocalDataState current = _requireState(); final List ideas = current.ideas .map((RelationshipIdea item) => item.id == idea.id ? idea : item) .toList(growable: false); await _setState(current.copyWith(ideas: ideas)); } Future toggleIdeaArchived(String ideaId) async { final LocalDataState current = _requireState(); final List ideas = current.ideas .map( (RelationshipIdea item) => item.id == ideaId ? item.copyWith(isArchived: !item.isArchived) : item, ) .toList(growable: false); await _setState(current.copyWith(ideas: ideas)); } Future deleteIdea(String ideaId) async { final LocalDataState current = _requireState(); final List ideas = current.ideas .where((RelationshipIdea idea) => idea.id != ideaId) .toList(growable: false); await _setState(current.copyWith(ideas: ideas)); } Future addReminder({ required String title, required ReminderCadence cadence, required DateTime nextAt, String? personId, }) async { final String normalizedTitle = title.trim(); if (normalizedTitle.isEmpty) { throw ArgumentError('Reminder title is required'); } final LocalDataState current = _requireState(); final ReminderRule reminder = ReminderRule( id: 'r-${_uuid.v4()}', personId: personId, title: normalizedTitle, cadence: cadence, nextAt: nextAt, enabled: true, ); final List reminders = [ reminder, ...current.reminders, ]; await _setState(current.copyWith(reminders: reminders)); } Future updateReminder(ReminderRule reminder) async { if (reminder.title.trim().isEmpty) { throw ArgumentError('Reminder title is required'); } final LocalDataState current = _requireState(); final List reminders = current.reminders .map((ReminderRule item) => item.id == reminder.id ? reminder : item) .toList(growable: false); await _setState(current.copyWith(reminders: reminders)); } Future toggleReminderEnabled(String reminderId) async { final LocalDataState current = _requireState(); final List reminders = current.reminders .map( (ReminderRule item) => item.id == reminderId ? item.copyWith(enabled: !item.enabled) : item, ) .toList(growable: false); await _setState(current.copyWith(reminders: reminders)); } Future deleteReminder(String reminderId) async { final LocalDataState current = _requireState(); final List reminders = current.reminders .where((ReminderRule reminder) => reminder.id != reminderId) .toList(growable: false); await _setState(current.copyWith(reminders: reminders)); } Future toggleTaskDone(String taskId) async { final LocalDataState current = _requireState(); final List tasks = current.tasks .map( (DashboardTask task) => task.id == taskId ? task.copyWith(done: !task.done) : task, ) .toList(growable: false); await _setState(current.copyWith(tasks: tasks)); } Future _setState(LocalDataState next) async { state = AsyncData(next); await _persist(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 { final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setString(_storageKey, jsonEncode(state.toJson())); await prefs.setInt(_schemaVersionKey, _schemaVersion); } Future _migrateIfNeeded(SharedPreferences prefs) async { final int currentVersion = prefs.getInt(_schemaVersionKey) ?? 0; if (currentVersion == _schemaVersion) { return; } if (currentVersion <= 0) { await prefs.remove(_storageKey); await prefs.setInt(_schemaVersionKey, _schemaVersion); return; } // Migration placeholder for future schema versions. await prefs.setInt(_schemaVersionKey, _schemaVersion); } String _titleFromSummary(String summary) { final List words = summary.split(RegExp(r'\s+')); final int take = words.length < 5 ? words.length : 5; return words.take(take).join(' '); } } final AsyncNotifierProvider localRepositoryProvider = AsyncNotifierProvider(LocalRepository.new);