import 'dart:async'; import 'package:flutter/foundation.dart' show kIsWeb; import '../data/local/app_database.dart'; import '../data/local/user_profile_local_store.dart'; import '../data/remote/profile_api_exception.dart'; import '../data/remote/user_profile_remote_store.dart'; import '../data/sync/connectivity_service.dart'; import '../data/sync/profile_sync_coordinator.dart'; import '../models/app_user.dart'; import '../models/sync_result.dart'; import '../models/user_profile.dart'; /// Facade for profile reads/writes with Drift (native) and API sync. class UserProfileRepository { UserProfileRepository._(); static final UserProfileRepository instance = UserProfileRepository._(); final ConnectivityService _connectivity = ConnectivityService(); final UserProfileRemoteStore _remote = UserProfileRemoteStore(); AppDatabase? _database; UserProfileLocalStore? _local; ProfileSyncCoordinator? _sync; StreamSubscription? _connectivitySubscription; final StreamController _profileController = StreamController.broadcast(); final StreamController _syncStatusController = StreamController.broadcast(); StreamSubscription? _localWatchSubscription; String? _activeUid; UserProfile? _cachedProfile; Stream get profileStream => _profileController.stream; Stream get syncStatusStream => _syncStatusController.stream; UserProfile? get currentProfile => _cachedProfile; bool get usesLocalStore => _local != null; Future initialize() async { await _connectivity.initialize(); if (!kIsWeb) { _database = AppDatabase(); _local = UserProfileLocalStore(_database!); _sync = ProfileSyncCoordinator( local: _local, remote: _remote, connectivity: _connectivity, ); } else { _sync = ProfileSyncCoordinator( local: null, remote: _remote, connectivity: _connectivity, ); } _connectivitySubscription = _connectivity.onlineChanges.listen(( bool online, ) { if (online && _activeUid != null) { unawaited(sync()); } else if (!online) { _emitSyncStatus(ProfileSyncStatus.offline); } }); _emitSyncStatus(ProfileSyncStatus.idle); } Future startSession(AppUser user) async { if (_activeUid == user.uid) { return; } await endSession(); _activeUid = user.uid; _emitSyncStatus(ProfileSyncStatus.syncing); if (_local != null) { _localWatchSubscription = _local!.watchProfile(user.uid).listen( (UserProfile? profile) { _cachedProfile = profile; _profileController.add(profile); }, ); UserProfile? profile = await _local!.getProfile(user.uid); profile ??= UserProfile.fromAuth( firebaseUid: user.uid, email: user.email, displayName: user.displayName, photoUrl: user.photoUrl, ); await _local!.saveProfile(profile, dirty: true); _cachedProfile = profile; _profileController.add(profile); } else { await _loadRemoteProfile(user); } await sync(); } Future _loadRemoteProfile(AppUser user) async { try { UserProfile? profile = await _remote.fetchProfile(); profile ??= UserProfile.fromAuth( firebaseUid: user.uid, email: user.email, displayName: user.displayName, photoUrl: user.photoUrl, ); if (profile.revision == 1 && !profile.dirty) { final UserProfile seeded = profile.copyWith(dirty: true); profile = await _remote.pushProfile(seeded); } _cachedProfile = profile; _profileController.add(profile); } catch (_) { final UserProfile fallback = UserProfile.fromAuth( firebaseUid: user.uid, email: user.email, displayName: user.displayName, photoUrl: user.photoUrl, ); _cachedProfile = fallback; _profileController.add(fallback); _emitSyncStatus(ProfileSyncStatus.error); } } Future endSession() async { await _localWatchSubscription?.cancel(); _localWatchSubscription = null; if (_activeUid != null && _local != null) { await _local!.clearLocal(_activeUid!); } _activeUid = null; _cachedProfile = null; _profileController.add(null); _emitSyncStatus(ProfileSyncStatus.idle); } Future updateProfile(UserProfile profile) async { final UserProfile updated = profile.copyWith( revision: profile.revision + 1, updatedAt: DateTime.now().toUtc(), dirty: true, ); if (_local != null) { await _local!.saveProfile(updated, dirty: true); } else { _cachedProfile = updated; _profileController.add(updated); await _remote.pushProfile(updated); } unawaited(sync()); } Future sync() async { final String? uid = _activeUid; if (uid == null || _sync == null) { return const SyncResult.error('No active session'); } if (!kIsWeb && !_connectivity.isOnline) { _emitSyncStatus(ProfileSyncStatus.offline); return const SyncResult.offline(); } _emitSyncStatus(ProfileSyncStatus.syncing); final SyncResult result = _local == null ? await _syncWebOnly() : await _sync!.sync(uid); switch (result.kind) { case SyncResultKind.success: case SyncResultKind.conflictResolved: _emitSyncStatus(ProfileSyncStatus.synced); case SyncResultKind.offline: _emitSyncStatus(ProfileSyncStatus.offline); case SyncResultKind.error: _emitSyncStatus(ProfileSyncStatus.error); } return result; } Future _syncWebOnly() async { final UserProfile? profile = _cachedProfile; if (profile == null) { return const SyncResult.error('No profile loaded'); } try { if (profile.dirty) { try { final UserProfile saved = await _remote.pushProfile(profile); _cachedProfile = saved; _profileController.add(saved); } on ProfileConflictException catch (e) { _cachedProfile = e.serverProfile; _profileController.add(e.serverProfile); return const SyncResult.conflictResolved(); } } else { final UserProfile? server = await _remote.fetchProfile(); if (server != null) { _cachedProfile = server; _profileController.add(server); } } return const SyncResult.success(); } on ProfileApiException catch (e) { return SyncResult.error(e.toString()); } catch (e) { return SyncResult.error(e.toString()); } } Future dispose() async { await _connectivitySubscription?.cancel(); _connectivity.dispose(); await _localWatchSubscription?.cancel(); await _profileController.close(); await _syncStatusController.close(); await _database?.close(); } void _emitSyncStatus(ProfileSyncStatus status) { if (!_syncStatusController.isClosed) { _syncStatusController.add(status); } } }