import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/remote/remote_settings.dart'; import 'package:aves/remote/remote_sync_bus.dart'; import 'package:aves/remote/remote_repository.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/remote/remote_client.dart'; import 'package:aves/remote/auth_client.dart'; import 'package:aves/remote/collection_source_remote_ext.dart'; import 'remote_origin.dart'; class RemoteController { RemoteController._(); static final RemoteController instance = RemoteController._(); static const _kBootstrapDone = 'remote_bootstrap_done'; bool _syncInFlight = false; // πŸ”₯ Retry basato sul tempo reale DateTime? _retryStartTime; Timer? _retryTimer; Future bootstrapDone() async { final storage = FlutterSecureStorage(); return (await storage.read(key: _kBootstrapDone)) == '1'; } Future _setBootstrapDone() async { final storage = FlutterSecureStorage(); await storage.write(key: _kBootstrapDone, value: '1'); } // πŸ”₯ Stato iniziale corretto: arancione lampeggiante Future initBusFromSettings() async { final s = await RemoteSettings.load(); if (!s.enabled) { RemoteSyncBus.instance.setDisabled(); return; } RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.syncing; } Future onAppStart({ required CollectionSource source, bool resumeBootstrapIfEnabled = true, }) async { final s = await RemoteSettings.load(); if (!s.enabled) { RemoteSyncBus.instance.setDisabled(); final remotesInMemory = source.allEntries.where((e) => e.origin == RemoteOrigin.value).toSet(); if (remotesInMemory.isNotEmpty) { source.removeEntriesFromMemory(remotesInMemory); } return; } final done = await bootstrapDone(); if (done) { await source.appendRemoteEntriesFromDb(); unawaited(fullSync(source: source, showOverlay: false)); } else { if (resumeBootstrapIfEnabled) { unawaited(fullSync( source: source, showOverlay: true, markBootstrapDoneOnSuccess: true, )); } } } Future toggleRemote({required CollectionSource source}) async { final s = await RemoteSettings.load(); if (s.enabled) { final upd = RemoteSettings( enabled: false, baseUrl: s.baseUrl, indexPath: s.indexPath, email: s.email, password: s.password, ); await upd.save(); debugPrint('[remote] toggle -> OFF'); final remotesInMemory = source.allEntries.where((e) => e.origin == RemoteOrigin.value).toSet(); if (remotesInMemory.isNotEmpty) { source.removeEntriesFromMemory(remotesInMemory); } RemoteSyncBus.instance.setDisabled(); _retryTimer?.cancel(); _retryTimer = null; _retryStartTime = null; return; } final upd = RemoteSettings( enabled: true, baseUrl: s.baseUrl, indexPath: s.indexPath, email: s.email, password: s.password, ); await upd.save(); debugPrint('[remote] toggle -> ON'); await source.appendRemoteEntriesFromDb(); unawaited(fullSync(source: source, showOverlay: false)); } // πŸ”₯ Retry basato sul tempo reale void _scheduleRetry(CollectionSource source) { _retryTimer?.cancel(); _retryTimer = Timer(const Duration(seconds: 30), () async { final s = await RemoteSettings.load(); if (!s.enabled) return; // πŸ”₯ Controllo tempo reale passato if (_retryStartTime != null) { final elapsed = DateTime.now().difference(_retryStartTime!); if (elapsed > const Duration(minutes: 5)) { print('[remote] retry timeout β†’ disattivo remote'); final remotesInMemory = source.allEntries.where((e) => e.origin == RemoteOrigin.value).toSet(); if (remotesInMemory.isNotEmpty) { source.removeEntriesFromMemory(remotesInMemory); } final upd = RemoteSettings( enabled: false, baseUrl: s.baseUrl, indexPath: s.indexPath, email: s.email, password: s.password, ); await upd.save(); RemoteSyncBus.instance.setDisabled(); return; } } print('[remote] retry ping…'); final auth = (s.email.isNotEmpty && s.password.isNotEmpty) ? RemoteAuth( baseUrl: s.baseUrl, email: s.email, password: s.password, ) : null; final retryClient = RemoteJsonClient( s.baseUrl, s.indexPath, auth: auth, ); try { await retryClient.ping().timeout(const Duration(seconds: 3)); print('[remote] retry OK β†’ riprendo sync'); _retryTimer = null; _retryStartTime = null; unawaited(fullSync(source: source, showOverlay: false)); return; } catch (_) { print('[remote] retry fallito'); _scheduleRetry(source); } }); } // πŸ”₯ fullSync pulito, senza retry interno Future fullSync({ required CollectionSource source, required bool showOverlay, bool markBootstrapDoneOnSuccess = false, }) async { if (_syncInFlight) { debugPrint('[remote] sync skipped (already in flight)'); return; } _syncInFlight = true; final s = await RemoteSettings.load(); if (!s.enabled) { RemoteSyncBus.instance.setDisabled(); _syncInFlight = false; return; } try { if (s.baseUrl.trim().isEmpty) { RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown; _retryStartTime ??= DateTime.now(); _scheduleRetry(source); return; } RemoteAuth? auth; if (s.email.isNotEmpty && s.password.isNotEmpty) { auth = RemoteAuth( baseUrl: s.baseUrl, email: s.email, password: s.password, ); } final client = RemoteJsonClient(s.baseUrl, s.indexPath, auth: auth); // 1️⃣ PING await client.ping().timeout(const Duration(seconds: 3)); // 2️⃣ FETCH LISTA final items = await client.fetchAll().timeout(const Duration(seconds: 30)); final total = items.length; // 3️⃣ BANNER final opId = RemoteSyncBus.instance.start(total: total, showOverlay: showOverlay); final repo = RemoteRepository(localMediaDb.rawDb); await repo.deleteAllRemotes(); const chunkSize = 200; int done = 0; final serverIds = items.map((e) => e.id).where((v) => v.isNotEmpty).toSet(); for (var offset = 0; offset < total; offset += chunkSize) { final end = (offset + chunkSize < total) ? offset + chunkSize : total; await repo.upsertAll(items.sublist(offset, end), chunkSize: chunkSize); done = end; RemoteSyncBus.instance.update( opId: opId, done: done, total: total); } await repo.pruneMissingRemotes(serverIds); await source.appendRemoteEntriesFromDb(); if (markBootstrapDoneOnSuccess) { await _setBootstrapDone(); } RemoteSyncBus.instance.finishUpToDate(opId: opId); // πŸ”₯ Sync OK β†’ stop retry _retryTimer?.cancel(); _retryTimer = null; _retryStartTime = null; } catch (e) { print('[remote] error during sync: $e'); final remotesInMemory = source.allEntries.where((e) => e.origin == RemoteOrigin.value).toSet(); if (remotesInMemory.isNotEmpty) { source.removeEntriesFromMemory(remotesInMemory); } RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown; // πŸ”₯ Avvia retry esterno basato sul tempo reale _retryStartTime ??= DateTime.now(); _scheduleRetry(source); } finally { _syncInFlight = false; } } // πŸ”₯ Ping immediato al resume dell’app Future onResume(CollectionSource source) async { final s = await RemoteSettings.load(); if (!s.enabled) return; // Se siamo in retry, controlla timeout if (_retryStartTime != null) { final elapsed = DateTime.now().difference(_retryStartTime!); if (elapsed > const Duration(minutes: 5)) { print('[remote] resume β†’ timeout superato β†’ disattivo remote'); final remotesInMemory = source.allEntries.where((e) => e.origin == RemoteOrigin.value).toSet(); if (remotesInMemory.isNotEmpty) { source.removeEntriesFromMemory(remotesInMemory); } final upd = RemoteSettings( enabled: false, baseUrl: s.baseUrl, indexPath: s.indexPath, email: s.email, password: s.password, ); await upd.save(); RemoteSyncBus.instance.setDisabled(); return; } } print('[remote] resume β†’ ping immediato'); final auth = (s.email.isNotEmpty && s.password.isNotEmpty) ? RemoteAuth( baseUrl: s.baseUrl, email: s.email, password: s.password, ) : null; final client = RemoteJsonClient( s.baseUrl, s.indexPath, auth: auth, ); try { await client.ping().timeout(const Duration(seconds: 3)); print('[remote] resume β†’ ping OK β†’ sync immediata'); _retryTimer?.cancel(); _retryTimer = null; _retryStartTime = null; unawaited(fullSync(source: source, showOverlay: false)); } catch (_) { print('[remote] resume β†’ ping fallito β†’ scheduleRetry'); _retryStartTime ??= DateTime.now(); _scheduleRetry(source); } } }