import 'package:flutter/foundation.dart'; import 'package:aves/model/source/collection_source.dart'; import 'remote_http_api.dart'; import 'remote_models.dart'; import 'remote_repository.dart'; import 'remote_state_store.dart'; import 'package:aves/remote/collection_source_remote_ext.dart'; class RemoteSyncEngine { RemoteSyncEngine({ required this.api, required this.repo, required this.source, required this.state, }); final RemoteHttpApi api; final RemoteRepository repo; final CollectionSource source; final RemoteStateStore state; static const int retentionDays = 30; static const Duration overlap = Duration(seconds: 3); bool _syncInFlight = false; bool _pendingSync = false; bool _isTooOld(String iso) { final t = DateTime.tryParse(iso); if (t == null) return true; return DateTime.now().toUtc().difference(t.toUtc()).inDays > retentionDays; } String _applyOverlap(String iso) { final t = DateTime.tryParse(iso); if (t == null) return iso; return t.toUtc().subtract(overlap).toIso8601String(); } // ------------------------------------------------------------ // Public API // ------------------------------------------------------------ Future progressiveSync() async { return _runExclusive(() async { final last = await state.getLastSyncIso(); if (last == null || last.isEmpty || _isTooOld(last)) { await _fullSyncImpl(); return; } await _deltaSyncImpl(_applyOverlap(last)); }); } Future progressiveSyncFrom(String sinceIso) async { return _runExclusive(() async { if (sinceIso.isEmpty || _isTooOld(sinceIso)) { await _fullSyncImpl(); return; } await _deltaSyncImpl(_applyOverlap(sinceIso)); }); } // ------------------------------------------------------------ // FULL SYNC (senza _runExclusive) // ------------------------------------------------------------ Future _fullSyncImpl() async { debugPrint('[remote] fullSync start'); final list = await api.getAllPhotos(); final items = list.map(RemotePhotoItem.fromJson).toList(); await repo.deleteAllRemotes(); await repo.upsertAll(items, chunkSize: 200); await source.appendRemoteEntriesFromDb(); final next = _maxIsoFromItems(items) ?? DateTime.now().toUtc().toIso8601String(); await state.setLastSyncIso(next); debugPrint('[remote] fullSync done items=${items.length} lastSync=$next'); } // ------------------------------------------------------------ // DELTA SYNC (senza _runExclusive) // ------------------------------------------------------------ Future _deltaSyncImpl(String sinceIso, {bool appendAfter = true}) async { debugPrint('[remote] deltaSyncFrom since=$sinceIso'); final List> changed = await api.getChanges(sinceIso); final List> deleted = await api.getDeletedHard(sinceIso); var didChange = false; List changedItems = const []; if (changed.isNotEmpty) { changedItems = changed.map(RemotePhotoItem.fromJson).toList(); await repo.upsertAll(changedItems, chunkSize: 200); debugPrint('[remote] delta upsert=${changedItems.length}'); didChange = true; } if (deleted.isNotEmpty) { final ids = deleted .map((e) => e['id']?.toString()) .whereType() .where((s) => s.isNotEmpty) .toSet(); if (ids.isNotEmpty) { await repo.deleteRemotesByRemoteIds(ids); debugPrint('[remote] delta hardDeleted=${ids.length}'); didChange = true; } } if (appendAfter && didChange) { await source.appendRemoteEntriesFromDb(); } final next = _computeNextSyncIso( changedItems: changedItems, hardDeletedRaw: deleted, ) ?? DateTime.now().toUtc().toIso8601String(); await state.setLastSyncIso(next); debugPrint('[remote] delta lastSync -> $next'); } // ------------------------------------------------------------ // Exclusive wrapper (FIXATO) // ------------------------------------------------------------ Future _runExclusive(Future Function() fn) async { if (_syncInFlight) { _pendingSync = true; debugPrint('[remote] sync already in flight -> pending=true'); return; } _syncInFlight = true; try { await fn(); } finally { _syncInFlight = false; if (_pendingSync) { _pendingSync = false; debugPrint('[remote] run pending sync'); // ❗ NON ricorsivo Future.microtask(() => _runExclusive(fn)); } } } // ------------------------------------------------------------ // Helpers // ------------------------------------------------------------ String? _maxIsoFromItems(List items) { DateTime? maxDt; for (final it in items) { final dt = it.updatedAtUtc ?? it.createdAtUtc; if (dt != null && (maxDt == null || dt.isAfter(maxDt))) { maxDt = dt; } } return maxDt?.toUtc().toIso8601String(); } String? _computeNextSyncIso({ required List changedItems, required List> hardDeletedRaw, }) { DateTime? maxDt; void consider(DateTime? dt) { if (dt == null) return; if (maxDt == null || dt.isAfter(maxDt!)) maxDt = dt; } for (final it in changedItems) { consider(it.updatedAtUtc ?? it.createdAtUtc); } for (final d in hardDeletedRaw) { final s = d['deleted_at']?.toString(); if (s != null && s.isNotEmpty) { consider(DateTime.tryParse(s)?.toUtc()); } } return maxDt?.toUtc().toIso8601String(); } }