import 'package:dartz/dartz.dart'; import '../../core/errors/exceptions.dart'; import '../../core/errors/failures.dart'; import '../data_sources/local/server_local_data_source.dart'; import '../data_sources/remote/server_remote_data_source.dart'; import '../data_sources/local/action_mapping_local_data_source.dart'; import '../data_sources/remote/action_mapping_remote_data_source.dart'; enum SyncStatus { idle, syncing, success, error, } class SyncResult { final SyncStatus status; final String? message; final int? syncedCount; final List? errors; SyncResult({ required this.status, this.message, this.syncedCount, this.errors, }); factory SyncResult.idle() => SyncResult(status: SyncStatus.idle); factory SyncResult.syncing() => SyncResult( status: SyncStatus.syncing, message: 'Syncing changes...', ); factory SyncResult.success(int count) => SyncResult( status: SyncStatus.success, message: 'Successfully synced $count change${count != 1 ? 's' : ''}', syncedCount: count, ); factory SyncResult.error(List errors) => SyncResult( status: SyncStatus.error, message: 'Sync failed: ${errors.length} error${errors.length != 1 ? 's' : ''}', errors: errors, ); } abstract class SyncService { /// Sync all dirty servers with the remote API Future> syncServers(); /// Download fresh data from server and update local storage Future> downloadServers(); /// Sync all dirty action mappings with the remote API Future> syncActionMappings(); /// Download fresh action mappings from server and update local storage Future> downloadActionMappings(); } class SyncServiceImpl implements SyncService { final ServerLocalDataSource localDataSource; final ServerRemoteDataSource remoteDataSource; final ActionMappingLocalDataSource actionMappingLocalDataSource; final ActionMappingRemoteDataSource actionMappingRemoteDataSource; SyncServiceImpl({ required this.localDataSource, required this.remoteDataSource, required this.actionMappingLocalDataSource, required this.actionMappingRemoteDataSource, }); @override Future> syncServers() async { try { // Get all dirty servers final dirtyServers = await localDataSource.getDirtyServers(); if (dirtyServers.isEmpty) { return Right(SyncResult.success(0)); } final errors = []; int successCount = 0; // Process each dirty server for (final server in dirtyServers) { try { final operation = server.syncOperation; final serverModel = server.toServerModel(); if (operation == 'create') { await remoteDataSource.createServer(serverModel); await localDataSource.markServerAsSynced(server.id, server.serverType); successCount++; } else if (operation == 'update') { await remoteDataSource.updateServer(serverModel); await localDataSource.markServerAsSynced(server.id, server.serverType); successCount++; } else if (operation == 'delete') { await remoteDataSource.deleteServer(server.id, server.serverType); await localDataSource.markServerAsSynced(server.id, server.serverType); successCount++; } } catch (e) { errors.add('Failed to sync ${server.alias}: ${e.toString()}'); } } if (errors.isEmpty) { return Right(SyncResult.success(successCount)); } else if (successCount > 0) { // Partial success return Right(SyncResult( status: SyncStatus.success, message: 'Synced $successCount/${dirtyServers.length} changes', syncedCount: successCount, errors: errors, )); } else { // Complete failure return Right(SyncResult.error(errors)); } } on ServerException catch (e) { return Left(ServerFailure(e.message)); } on NetworkException catch (e) { return Left(NetworkFailure(e.message)); } catch (e) { return Left(ServerFailure('Unexpected error during sync: ${e.toString()}')); } } @override Future> downloadServers() async { try { // Fetch all servers from API final servers = await remoteDataSource.getAllServers(); // Replace local storage (preserving dirty servers) await localDataSource.replaceAllServers(servers); return Right(servers.length); } on ServerException catch (e) { return Left(ServerFailure(e.message)); } on NetworkException catch (e) { return Left(NetworkFailure(e.message)); } catch (e) { return Left(ServerFailure('Unexpected error during download: ${e.toString()}')); } } @override Future> syncActionMappings() async { try { // Get all dirty action mappings final dirtyMappings = await actionMappingLocalDataSource.getDirtyActionMappings(); if (dirtyMappings.isEmpty) { return Right(SyncResult.success(0)); } final errors = []; int successCount = 0; // Process each dirty action mapping for (final mapping in dirtyMappings) { try { final operation = mapping.syncOperation; final mappingModel = mapping.toActionMappingModel(); if (operation == 'create') { await actionMappingRemoteDataSource.createActionMapping(mappingModel); await actionMappingLocalDataSource.markActionMappingAsSynced(mapping.id); successCount++; } else if (operation == 'update') { await actionMappingRemoteDataSource.updateActionMapping(mapping.id, mappingModel); await actionMappingLocalDataSource.markActionMappingAsSynced(mapping.id); successCount++; } else if (operation == 'delete') { await actionMappingRemoteDataSource.deleteActionMapping(mapping.id); await actionMappingLocalDataSource.markActionMappingAsSynced(mapping.id); successCount++; } } catch (e) { errors.add('Failed to sync ${mapping.name}: ${e.toString()}'); } } if (errors.isEmpty) { return Right(SyncResult.success(successCount)); } else if (successCount > 0) { // Partial success return Right(SyncResult( status: SyncStatus.success, message: 'Synced $successCount/${dirtyMappings.length} changes', syncedCount: successCount, errors: errors, )); } else { // Complete failure return Right(SyncResult.error(errors)); } } on ActionMappingException catch (e) { return Left(ActionMappingFailure(e.message)); } on NetworkException catch (e) { return Left(NetworkFailure(e.message)); } catch (e) { return Left(ActionMappingFailure('Unexpected error during sync: ${e.toString()}')); } } @override Future> downloadActionMappings() async { try { // Fetch all action mappings from API final mappings = await actionMappingRemoteDataSource.getAllActionMappings(); // Replace local storage with force=true to discard local changes and get fresh server data await actionMappingLocalDataSource.replaceAllActionMappings(mappings, force: true); return Right(mappings.length); } on ActionMappingException catch (e) { return Left(ActionMappingFailure(e.message)); } on NetworkException catch (e) { return Left(NetworkFailure(e.message)); } catch (e) { return Left(ActionMappingFailure('Unexpected error during download: ${e.toString()}')); } } }