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 esterno Timer? _retryTimer; int _retryCount = 0; 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(); // 🔥 Stop retry loop _retryTimer?.cancel(); _retryTimer = 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 esterno void _startRetryLoop(CollectionSource source) { _retryTimer?.cancel(); _retryCount = 0; _retryTimer = Timer.periodic(const Duration(seconds: 30), (timer) async { _retryCount++; print('[remote] retry $_retryCount/10'); final s = await RemoteSettings.load(); if (!s.enabled) { timer.cancel(); return; } 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'); timer.cancel(); _retryTimer = null; unawaited(fullSync(source: source, showOverlay: false)); return; } catch (_) { print('[remote] retry fallito'); } if (_retryCount >= 10) { print('[remote] server down per troppo tempo → disattivo remote'); timer.cancel(); _retryTimer = null; 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(); } }); } // 🔥 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; _startRetryLoop(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); // 🔥 Se la sync va bene → stop retry _retryTimer?.cancel(); _retryTimer = 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 _startRetryLoop(source); } finally { _syncInFlight = false; } } }