part of 'relationship_repository.dart'; /// Sync queue integration and change-envelope serialization helpers. extension LocalRepositorySyncOperations on LocalRepository { Future applyRemoteChanges(List changes) async { if (changes.isEmpty) { return; } LocalDataState next = _requireState(); for (final ChangeEnvelope change in changes) { next = _applyRemoteChange(next, change); } await _setState(next); } LocalDataState _applyRemoteChange( LocalDataState current, ChangeEnvelope change, ) { switch (change.entityType) { case 'person': return _applyPersonChange(current, change); case 'capture': return _applyMomentChange(current, change); case 'giftIdea': case 'eventIdea': return _applyIdeaChange(current, change); case 'reminderRule': return _applyReminderChange(current, change); default: return current; } } LocalDataState _applyPersonChange( LocalDataState current, ChangeEnvelope change, ) { if (change.op == ChangeOperation.delete) { final String personId = change.entityId; return current.copyWith( people: current.people .where((PersonProfile person) => person.id != personId) .toList(growable: false), moments: current.moments .where((RelationshipMoment moment) => moment.personId != personId) .toList(growable: false), ideas: current.ideas .where((RelationshipIdea idea) => idea.personId != personId) .toList(growable: false), reminders: current.reminders .where((ReminderRule reminder) => reminder.personId != personId) .toList(growable: false), preferenceSignals: current.preferenceSignals .where( (PersonPreferenceSignal signal) => signal.personId != personId, ) .toList(growable: false), personFacts: current.personFacts .where((PersonFact fact) => fact.personId != personId) .toList(growable: false), aiSuggestionDrafts: current.aiSuggestionDrafts .where((AiSuggestionDraft draft) => draft.personId != personId) .toList(growable: false), importantDates: current.importantDates .where((PersonImportantDate value) => value.personId != personId) .toList(growable: false), sourceLinks: current.sourceLinks .where((SourceProfileLink link) => link.profileId != personId) .toList(growable: false), sharedMessages: current.sharedMessages .where((SharedMessageEntry entry) => entry.profileId != personId) .toList(growable: false), ); } final int existingIndex = current.people.indexWhere( (PersonProfile person) => person.id == change.entityId, ); final PersonProfile? existing = existingIndex >= 0 ? current.people[existingIndex] : null; final Map payload = change.payload ?? {}; final PersonProfile next = PersonProfile( id: change.entityId, name: _stringField(payload, 'name', existing?.name ?? 'Unknown'), relationship: _stringField( payload, 'relationship', existing?.relationship ?? 'Relationship', ), affinityScore: _intField( payload, 'affinityScore', existing?.affinityScore ?? 70, ), nextMoment: _dateField( payload, 'nextMoment', existing?.nextMoment ?? DateTime.now().add(const Duration(days: 3)), ), tags: _stringListField(payload, 'tags', existing?.tags ?? []), notes: _stringField(payload, 'notes', existing?.notes ?? ''), aliases: _stringListField( payload, 'aliases', existing?.aliases ?? [], ), location: _stringOrNullField(payload, 'location', existing?.location), lastUpdatedAt: _dateOrNullField( payload, 'lastUpdatedAt', existing?.lastUpdatedAt, ), lastInteractedAt: _dateOrNullField( payload, 'lastInteractedAt', existing?.lastInteractedAt, ), ); final List people = _upsertItem( items: current.people, item: next, idOf: (PersonProfile person) => person.id, ); return current.copyWith(people: people); } LocalDataState _applyMomentChange( LocalDataState current, ChangeEnvelope change, ) { if (change.op == ChangeOperation.delete) { return current.copyWith( moments: current.moments .where((RelationshipMoment moment) => moment.id != change.entityId) .toList(growable: false), ); } final int existingIndex = current.moments.indexWhere( (RelationshipMoment moment) => moment.id == change.entityId, ); final RelationshipMoment? existing = existingIndex >= 0 ? current.moments[existingIndex] : null; final Map payload = change.payload ?? {}; final String summary = _stringField( payload, 'summary', existing?.summary ?? '', ); final RelationshipMoment next = RelationshipMoment( id: change.entityId, personId: _stringField( payload, 'personId', existing?.personId ?? (current.people.isEmpty ? 'person-unknown' : current.people.first.id), ), title: _stringField( payload, 'title', existing?.title ?? _titleFromSummary(summary), ), summary: summary, at: _dateField(payload, 'at', existing?.at ?? DateTime.now()), type: _stringField(payload, 'type', existing?.type ?? 'capture'), ); final List moments = _upsertItem( items: current.moments, item: next, idOf: (RelationshipMoment moment) => moment.id, ); return current.copyWith(moments: moments); } LocalDataState _applyIdeaChange( LocalDataState current, ChangeEnvelope change, ) { if (change.op == ChangeOperation.delete) { return current.copyWith( ideas: current.ideas .where((RelationshipIdea idea) => idea.id != change.entityId) .toList(growable: false), ); } final int existingIndex = current.ideas.indexWhere( (RelationshipIdea idea) => idea.id == change.entityId, ); final RelationshipIdea? existing = existingIndex >= 0 ? current.ideas[existingIndex] : null; final Map payload = change.payload ?? {}; final IdeaType fallbackType = _ideaTypeFromEntityType( change.entityType, fallback: existing?.type ?? IdeaType.gift, ); final IdeaType type = _ideaTypeFromRaw( payload['type'], fallback: fallbackType, ); final RelationshipIdea next = RelationshipIdea( id: change.entityId, personId: payload['personId'] as String? ?? existing?.personId, type: type, title: _stringField(payload, 'title', existing?.title ?? 'Untitled idea'), details: _stringField(payload, 'details', existing?.details ?? ''), createdAt: _dateField( payload, 'createdAt', existing?.createdAt ?? DateTime.now(), ), isArchived: _boolField( payload, 'isArchived', existing?.isArchived ?? false, ), ); final List ideas = _upsertItem( items: current.ideas, item: next, idOf: (RelationshipIdea idea) => idea.id, ); return current.copyWith(ideas: ideas); } LocalDataState _applyReminderChange( LocalDataState current, ChangeEnvelope change, ) { if (change.op == ChangeOperation.delete) { return current.copyWith( reminders: current.reminders .where((ReminderRule reminder) => reminder.id != change.entityId) .toList(growable: false), ); } final int existingIndex = current.reminders.indexWhere( (ReminderRule reminder) => reminder.id == change.entityId, ); final ReminderRule? existing = existingIndex >= 0 ? current.reminders[existingIndex] : null; final Map payload = change.payload ?? {}; final ReminderRule next = ReminderRule( id: change.entityId, personId: payload['personId'] as String? ?? existing?.personId, title: _stringField(payload, 'title', existing?.title ?? 'Reminder'), cadence: _cadenceFromRaw( payload['cadence'], fallback: existing?.cadence ?? ReminderCadence.weekly, ), nextAt: _dateField(payload, 'nextAt', existing?.nextAt ?? DateTime.now()), enabled: _boolField(payload, 'enabled', existing?.enabled ?? true), ); final List reminders = _upsertItem( items: current.reminders, item: next, idOf: (ReminderRule reminder) => reminder.id, ); return current.copyWith(reminders: reminders); } ChangeEnvelope _buildEnvelope({ required String entityType, required String entityId, required ChangeOperation op, Map? payload, }) { return ChangeEnvelope( schemaVersion: _syncSchemaVersion, entityType: entityType, entityId: entityId, op: op, modifiedAt: DateTime.now().toUtc(), clientMutationId: _uuid.v4(), payload: payload, ); } Map _personPayload(PersonProfile person) { return { 'name': person.name, 'relationship': person.relationship, 'affinityScore': person.affinityScore, 'nextMoment': person.nextMoment.toUtc().toIso8601String(), 'tags': person.tags, 'notes': person.notes, 'aliases': person.aliases, 'location': person.location, 'lastUpdatedAt': person.lastUpdatedAt?.toUtc().toIso8601String(), 'lastInteractedAt': person.lastInteractedAt?.toUtc().toIso8601String(), }; } Map _momentPayload(RelationshipMoment moment) { return { 'personId': moment.personId, 'title': moment.title, 'summary': moment.summary, 'at': moment.at.toUtc().toIso8601String(), 'type': moment.type, }; } Map _ideaPayload(RelationshipIdea idea) { return { 'personId': idea.personId, 'type': idea.type.name, 'title': idea.title, 'details': idea.details, 'createdAt': idea.createdAt.toUtc().toIso8601String(), 'isArchived': idea.isArchived, }; } Map _reminderPayload(ReminderRule reminder) { return { 'personId': reminder.personId, 'title': reminder.title, 'cadence': reminder.cadence.name, 'nextAt': reminder.nextAt.toUtc().toIso8601String(), 'enabled': reminder.enabled, }; } } String _ideaEntityType(IdeaType type) { return type == IdeaType.event ? 'eventIdea' : 'giftIdea'; } String _ideaEntityTypeFromId(List ideas, String ideaId) { final int index = ideas.indexWhere( (RelationshipIdea idea) => idea.id == ideaId, ); final RelationshipIdea? existing = index >= 0 ? ideas[index] : null; if (existing == null) { return 'giftIdea'; } return _ideaEntityType(existing.type); } IdeaType _ideaTypeFromEntityType( String entityType, { required IdeaType fallback, }) { switch (entityType) { case 'eventIdea': return IdeaType.event; case 'giftIdea': return IdeaType.gift; default: return fallback; } } IdeaType _ideaTypeFromRaw(dynamic value, {required IdeaType fallback}) { if (value is! String || value.isEmpty) { return fallback; } return IdeaType.values.firstWhere( (IdeaType type) => type.name == value, orElse: () => fallback, ); } ReminderCadence _cadenceFromRaw( dynamic value, { required ReminderCadence fallback, }) { if (value is! String || value.isEmpty) { return fallback; } return ReminderCadence.values.firstWhere( (ReminderCadence cadence) => cadence.name == value, orElse: () => fallback, ); } List _upsertItem({ required List items, required T item, required String Function(T value) idOf, }) { final int index = items.indexWhere((T value) => idOf(value) == idOf(item)); if (index < 0) { return [item, ...items]; } final List copy = items.toList(); copy[index] = item; return copy; } String _stringField(Map payload, String key, String fallback) { final dynamic value = payload[key]; if (value is String && value.trim().isNotEmpty) { return value.trim(); } return fallback; } String? _stringOrNullField( Map payload, String key, String? fallback, ) { final dynamic value = payload[key]; if (value is String) { final String trimmed = value.trim(); if (trimmed.isNotEmpty) { return trimmed; } return null; } return fallback; } int _intField(Map payload, String key, int fallback) { final dynamic value = payload[key]; if (value is int) { return value; } if (value is num) { return value.toInt(); } return fallback; } bool _boolField(Map payload, String key, bool fallback) { final dynamic value = payload[key]; if (value is bool) { return value; } return fallback; } List _stringListField( Map payload, String key, List fallback, ) { final dynamic value = payload[key]; if (value is! List) { return fallback; } return value.map((dynamic item) => '$item').toList(growable: false); } DateTime _dateField( Map payload, String key, DateTime fallback, ) { final dynamic value = payload[key]; if (value is String) { final DateTime? parsed = DateTime.tryParse(value); if (parsed != null) { return parsed.toLocal(); } } return fallback; } DateTime? _dateOrNullField( Map payload, String key, DateTime? fallback, ) { final dynamic value = payload[key]; if (value is String) { final DateTime? parsed = DateTime.tryParse(value); return parsed?.toLocal(); } return fallback; }