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'; class RemoteController { RemoteController._(); static final RemoteController instance = RemoteController._(); static const _kBootstrapDone = 'remote_bootstrap_done'; bool _syncInFlight = false; 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'); } /// Chiamare all’avvio: imposta lo stato icona (grigio/verde) coerente con settings. Future initBusFromSettings() async { final s = await RemoteSettings.load(); if (!s.enabled) { RemoteSyncBus.instance.setDisabled(); } else { // enabled: stato iniziale "upToDate" (poi la sync può cambiare) final opId = RemoteSyncBus.instance.start(total: 0, showOverlay: false); RemoteSyncBus.instance.finishUpToDate(opId: opId); } } /// Logica d’avvio app: /// - se remote OFF -> nascondi remoti dalla UI (memoria only) e stop /// - se remote ON: /// - se bootstrap done -> append DB immediato + sync silenzioso /// - se bootstrap NOT done -> opzionale resume bootstrap 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 == 1).toSet(); if (remotesInMemory.isNotEmpty) { source.removeEntriesFromMemory(remotesInMemory); } return; } final done = await bootstrapDone(); if (done) { await source.appendRemoteEntriesFromDb(); // sync in background (solo icona) unawaited(fullSync(source: source, showOverlay: false)); } else { if (resumeBootstrapIfEnabled) { unawaited(fullSync( source: source, showOverlay: true, markBootstrapDoneOnSuccess: true, )); } } } /// Toggle da icona (tap) Future toggleRemote({required CollectionSource source}) async { final s = await RemoteSettings.load(); if (s.enabled) { // TURN OFF final upd = RemoteSettings( enabled: false, baseUrl: s.baseUrl, indexPath: s.indexPath, email: s.email, password: s.password, ); await upd.save(); debugPrint('[remote] toggle -> enabled=false (OFF)'); // nascondi remoti (memoria only) final remotesInMemory = source.allEntries.where((e) => e.origin == 1).toSet(); if (remotesInMemory.isNotEmpty) { source.removeEntriesFromMemory(remotesInMemory); } // invalida sync in corso e icona grigia RemoteSyncBus.instance.setDisabled(); debugPrint('[remote] toggled OFF -> removed remotes from memory=${remotesInMemory.length}'); return; } // TURN ON final upd = RemoteSettings( enabled: true, baseUrl: s.baseUrl, indexPath: s.indexPath, email: s.email, password: s.password, ); await upd.save(); debugPrint('[remote] toggle -> enabled=true (ON)'); final first = !(await bootstrapDone()); if (first) { debugPrint('[remote] first enable -> FULL sync with overlay'); await fullSync( source: source, showOverlay: true, markBootstrapDoneOnSuccess: true, ); return; } debugPrint('[remote] enable -> append DB then background sync'); await source.appendRemoteEntriesFromDb(); unawaited(fullSync(source: source, showOverlay: false)); } /// Full sync remoto: /// - showOverlay=true solo bootstrap /// - showOverlay=false -> solo icona 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; } // Start: token opId (protezione anti-race) final opId = RemoteSyncBus.instance.start(total: 0, showOverlay: showOverlay); try { // base URL vuota -> server down if (s.baseUrl.trim().isEmpty) { debugPrint('[remote] serverDown (empty baseUrl)'); RemoteSyncBus.instance.failServerDown(opId: opId); 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); // fetch full list final items = await client.fetchAll().timeout(const Duration(seconds: 30)); final total = items.length; debugPrint('[remote] sync start overlay=$showOverlay total=$total'); // aggiorna total corretto RemoteSyncBus.instance.update(opId: opId, done: 0, total: total); 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); // mostra remoti in UI (dopo sync) await source.appendRemoteEntriesFromDb(); debugPrint('[remote] sync done'); if (markBootstrapDoneOnSuccess) { await _setBootstrapDone(); } RemoteSyncBus.instance.finishUpToDate(opId: opId); } on TimeoutException { debugPrint('[remote] serverDown (timeout)'); RemoteSyncBus.instance.failServerDown(opId: opId); } catch (e) { debugPrint('[remote] serverDown (error=$e)'); RemoteSyncBus.instance.failServerDown(opId: opId); } finally { _syncInFlight = false; } } }