import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/events.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 'package:aves/remote/collection_source_remote_ws_ext.dart';

import 'remote_origin.dart';
import 'remote_http_api.dart';
import 'remote_sync_engine.dart';
import 'remote_state_store.dart';
import 'remote_ws_client.dart';
import 'remote_http.dart';
import 'remote_added_queue.dart';


class RemoteController {
  RemoteController._();
  static final RemoteController instance = RemoteController._();

  static const _kBootstrapDone = 'remote_bootstrap_done';

  bool _syncInFlight = false;

  DateTime? _retryStartTime;
  Timer? _retryTimer;

  RemoteAuth? _auth;
  RemoteHttpApi? _api;
  RemoteSyncEngine? _engine;
  RemoteWsClient? _ws;
  RemoteStateStore? _stateStore;
  RemoteAddedQueue? _addedQueue;
  EventRingDeduper? _deduper;

  Future<void>? _ensureStackFuture;
  bool _wsConnectInFlight = false;

  // coalescing progressive
  Timer? _progressiveTimer;
  bool _progressiveScheduled = false;
  String? _pendingSinceIso;
  bool _progressiveInFlight = false;

  // ------------------------------------------------------------
  // helper: forza refresh lens quando cambia remoteVisible
  // ------------------------------------------------------------
  void _notifyRemoteVisibilityChanged(CollectionSource source) {
    try {
      source.eventBus.fire(const FilterVisibilityChangedEvent());
    } catch (_) {
      // ignore
    }
  }

  // refresh “forte” (la lens ascolta EntryRefreshedEvent e fa refresh())
  void _forceCollectionRefresh(CollectionSource source) {
    try {
      source.eventBus.fire(const EntryRefreshedEvent(<AvesEntry>{}));
    } catch (_) {
      // ignore
    }
  }


Future<void> _waitSourceReady(
  CollectionSource source, {
  required String reason,
  Duration timeout = const Duration(seconds: 45),
}) async {
  if (source.isReady) {
    debugPrint(
      '[remote_controller] source already ready '
      'reason=$reason '
      'state=${source.state}',
    );
    return;
  }

  debugPrint(
    '[remote_controller] waiting source ready '
    'reason=$reason '
    'state=${source.state}',
  );

  final completer = Completer<void>();
  Timer? timer;

  late VoidCallback listener;

  listener = () {
    if (source.isReady && !completer.isCompleted) {
      completer.complete();
    }
  };

  source.stateNotifier.addListener(listener);

  timer = Timer(timeout, () {
    if (!completer.isCompleted) {
      debugPrint(
        '[remote_controller] wait source ready timeout '
        'reason=$reason '
        'state=${source.state}',
      );
      completer.complete();
    }
  });

  try {
    listener();
    await completer.future;
  } finally {
    timer.cancel();
    source.stateNotifier.removeListener(listener);
  }

  debugPrint(
    '[remote_controller] source ready wait done '
    'reason=$reason '
    'state=${source.state}',
  );
}

  // ------------------------------------------------------------
  // bootstrap flag
  // ------------------------------------------------------------
  Future<bool> bootstrapDone() async {
    const storage = FlutterSecureStorage();
    return (await storage.read(key: _kBootstrapDone)) == '1';
  }

  Future<void> _setBootstrapDone() async {
    const storage = FlutterSecureStorage();
    await storage.write(key: _kBootstrapDone, value: '1');
  }

  // ------------------------------------------------------------
  // init bus (usata da HomePage init)
  // ------------------------------------------------------------
  Future<void> initBusFromSettings() async {
    final s = await RemoteSettings.load();
    if (!s.enabled) {
      RemoteSyncBus.instance.setDisabled();
      return;
    }
    RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.syncing;
    RemoteSyncBus.instance.progressNotifier.value = const RemoteSyncProgress(
      done: 0,
      total: 0,
      showOverlay: false,
    );
  }

  // ------------------------------------------------------------
  // ws url
  // ------------------------------------------------------------
  String _deriveWsUrl(String baseUrl) {
    final uri = Uri.parse(baseUrl);
    final scheme = uri.scheme == 'http' ? 'ws' : 'wss';
    final parts = uri.host.split('.');
    if (parts.isEmpty) return '$scheme://${uri.host}';
    parts[0] = '${parts[0]}-ws';
    return '$scheme://${parts.join('.')}';
  }

  String _resolveWsUrl(RemoteSettings s) =>
      s.wsUrl.trim().isNotEmpty ? s.wsUrl.trim() : _deriveWsUrl(s.baseUrl);

  // ------------------------------------------------------------
  // ensure stack single-flight
  // ------------------------------------------------------------
  Future<void> _ensureRealtimeStack(CollectionSource source) {
    final existing = _ensureStackFuture;
    if (existing != null) return existing;

    final fut = _ensureRealtimeStackImpl(source);
    _ensureStackFuture = fut;
    return fut.whenComplete(() {
      if (identical(_ensureStackFuture, fut)) _ensureStackFuture = null;
    });
  }

Future<void> _ensureRealtimeStackImpl(CollectionSource source) async {
  final s = await RemoteSettings.load();

  _stateStore ??= RemoteStateStore();
  _deduper ??= EventRingDeduper(_stateStore!);

  final wsUrl = _resolveWsUrl(s);

  final rebuildAuth =
      _auth == null ||
      _auth!.email != s.email ||
      _auth!.password != s.password ||
      _auth!.base.toString() != (s.baseUrl.endsWith('/') ? s.baseUrl : '${s.baseUrl}/');

  final rebuildWs = _ws == null || _ws!.wsUrl != wsUrl;

if (rebuildAuth || rebuildWs) {
  _auth = RemoteAuth(baseUrl: s.baseUrl, email: s.email, password: s.password);

  // allinea RemoteHttp (thumbs) allo stesso auth/token
  RemoteHttp.attach(baseUrl: s.baseUrl, auth: _auth!);
  await RemoteHttp.warmUp();

  _api = RemoteHttpApi(baseUrl: s.baseUrl, auth: _auth!);

  _engine = RemoteSyncEngine(
    api: _api!,
    repo: RemoteRepository(localMediaDb.rawDb),
    source: source,
    state: _stateStore!,
  );

  // chiudi eventuale ws precedente
  _ws?.close(); // ignore: unawaited_futures

  // crea la queue per batch di "added" con deduper e callback UI debounced
final addedQueue = RemoteAddedQueue(
  api: _api!,
  engine: _engine!,
  deduper: _deduper!,
  onUiRefresh: () {
    unawaited(() async {
      await source.appendRemoteEntriesFromDb();
      _notifyRemoteVisibilityChanged(source);
    }());
  },
);

  // dichiariamo la variabile prima per poterla referenziare nelle closure
  late RemoteWsClient wsClient;

wsClient = RemoteWsClient(
  wsUrl: wsUrl,
  store: _stateStore!,
  deduper: _deduper!,

  // Chiamato solo dopo auth_ok, oppure dopo recovery_done.
  // Solo qui i remoti possono diventare visibili.
  onReady: () {
    debugPrint('[remote_controller] WS ready -> set upToDate');

    RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.upToDate;
    RemoteSyncBus.instance.progressNotifier.value = null;

    _notifyRemoteVisibilityChanged(source);
  },

  // Chiamato se il WS cade o non autentica.
  // In questo caso i remoti devono restare nascosti.
  onDisconnected: () {
    debugPrint('[remote_controller] WS disconnected -> serverDown / hide remotes');

    final current = RemoteSyncBus.instance.stateNotifier.value;

    if (current != RemoteSyncState.disabled) {
      RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown;
      RemoteSyncBus.instance.progressNotifier.value = null;

      source.invalidateEntries();
      _notifyRemoteVisibilityChanged(source);
    }
  },

  // Quando il server richiede recovery, esegui progressive sync
  // e poi invia recovery_done. RemoteWsClient chiamerà onReady()
  // dopo notifyRecoveryCompleted().
  onNeedRecoverySync: () async {
    try {
      await _runProgressiveSync(source);
    } finally {
      wsClient.notifyRecoveryCompleted();
    }
  },

  // Delega gli altri eventi al controller.
  onEvent: (msg) => _handleWsEvent(source, msg),
);

  // registra la queue nel client WS
  wsClient.setAddedQueue(addedQueue);

  // salva la queue e il client nello stato del controller
  _addedQueue = addedQueue;
  _ws = wsClient;
  source.remoteWsClient = wsClient;

  // opzionale: inizializza la queue se ha init/async setup
  try {
    await _addedQueue?.init();
  } catch (_) {
    // ignore init errors, queue funzionerà comunque
  }
} else {
  await _deduper!.init();
}


}

  // ------------------------------------------------------------
  // progressive coalescing
  // ------------------------------------------------------------
  void _requestProgressiveSync({
    required CollectionSource source,
    String? sinceIso,
    Duration delay = const Duration(milliseconds: 600),
  }) {
    if (sinceIso != null && sinceIso.trim().isNotEmpty) {
      _pendingSinceIso = _minIso(_pendingSinceIso, sinceIso.trim());
    }

    if (_progressiveScheduled) return;
    _progressiveScheduled = true;

    _progressiveTimer?.cancel();
    _progressiveTimer = Timer(delay, () async {
      _progressiveScheduled = false;
      final since = _pendingSinceIso;
      _pendingSinceIso = null;

      try {
        await _runProgressiveSync(source, since: since);
      } catch (_) {}
    });
  }

  String? _minIso(String? a, String b) {
    if (a == null || a.isEmpty) return b;
    final da = DateTime.tryParse(a);
    final db = DateTime.tryParse(b);
    if (da == null) return b;
    if (db == null) return a;
    return db.isBefore(da) ? b : a;
  }

  // ------------------------------------------------------------
  // PATCH 1: progressive NON deve far tornare arancione se eri già verde
  // ------------------------------------------------------------
Future<void> _runProgressiveSync(
  CollectionSource source, {
  String? since,
}) async {
  if (_progressiveInFlight) {
    debugPrint(
      '[remote] progressive sync skipped: already in flight '
      'since=$since',
    );
    return;
  }

  _progressiveInFlight = true;

  try {
    await _ensureRealtimeStack(source);

    final engine = _engine;
    if (engine == null) return;

    final prev = RemoteSyncBus.instance.stateNotifier.value;

    // Se eri già verde, non far lampeggiare l'icona in arancione.
    final keepGreen = prev == RemoteSyncState.upToDate;

    int? opId;

    if (!keepGreen) {
      opId = RemoteSyncBus.instance.start(
        total: 0,
        showOverlay: false,
      );

      _notifyRemoteVisibilityChanged(source);
    }

    try {
      if (since != null && since.trim().isNotEmpty) {
        await engine.progressiveSyncFrom(since.trim());
      } else {
        await engine.progressiveSync();
      }

      if (opId != null) {
        RemoteSyncBus.instance.finishUpToDate(opId: opId);
      } else {
        // Resta verde.
        RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.upToDate;
        RemoteSyncBus.instance.progressNotifier.value = null;
      }

      // Importante:
      // NON chiamare computeFolderMap qui.
      // Il sync engine / appendRemoteEntriesFromDb aggiorna già DB/source/folderMap
      // quando serve. Qui basta notificare la UI.
      _notifyRemoteVisibilityChanged(source);
    } catch (e, st) {
      debugPrint('[remote] progressive sync failed: $e\n$st');

      if (opId != null) {
        RemoteSyncBus.instance.failServerDown(opId: opId);
      } else {
        RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown;
        RemoteSyncBus.instance.progressNotifier.value = null;
      }

      _notifyRemoteVisibilityChanged(source);

      rethrow;
    }
  } finally {
    _progressiveInFlight = false;
  }
}

  // ------------------------------------------------------------
  // ws start/stop
  // ------------------------------------------------------------
  Future<void> _startWsAlreadyEnsured() async {
    if (_wsConnectInFlight) return;
    _wsConnectInFlight = true;

    try {
      final s = await RemoteSettings.load();
      if (!s.enabled) return;
      if (_ws == null || _auth == null) return;
      if (_ws!.isConnected) return;

      final token = _auth!.token ?? await _auth!.login();
      await _ws!.connect(token: token);
    } finally {
      _wsConnectInFlight = false;
    }
  }

Future<void> _stopWs(CollectionSource source) async {
  _progressiveTimer?.cancel();
  _progressiveTimer = null;
  _progressiveScheduled = false;
  _pendingSinceIso = null;

  try {
    await _ws?.close();
  } catch (_) {}

  try {
    await _addedQueue?.dispose();
  } catch (_) {}

  _addedQueue = null;

  _ws = null;
  _engine = null;
  _api = null;
  _auth = null;
  _ensureStackFuture = null;
  _wsConnectInFlight = false;

  _notifyRemoteVisibilityChanged(source);
}

  // ------------------------------------------------------------
  // ws events
  // ------------------------------------------------------------

Future<bool> _handleWsEvent(CollectionSource source, Map<String, dynamic> msg) async {
  final type = msg['type']?.toString();
  if (type == null) return true;

  // BULK add_dir / del_dir: se il server fornisce "since", esegui subito la sync mirata
  if ((type == 'add_dir' || type == 'del_dir') && msg['mode'] == 'bulk') {
    final since = msg['since']?.toString();
    if (since != null && since.isNotEmpty) {
      try {
        await _runProgressiveSync(source, since: since);
        return true;
      } catch (_) {
        // se fallisce, segnala al caller che non è stato applicato
        return false;
      }
    }
    // se non c'è since, accetta l'evento e aspetta eventuale add_dir_done
    return true;
  }

  // add_dir_done / del_dir_done: server conferma fine bulk, esegui la sync mirata se presente
  if ((type == 'add_dir_done' || type == 'del_dir_done') && msg['mode'] == 'bulk') {
    final since = msg['since']?.toString();
    try {
      await _runProgressiveSync(source, since: since);
      return true;
    } catch (_) {
      return false;
    }
  }

  // added / del / removed / updated
  // - per added: la RemoteAddedQueue è già registrata nel WS client e gestisce batching
  // - per del/removed/updated: chiediamo una progressive coalesced (puoi ottimizzare per id)
  if (type == 'added' || type == 'del' || type == 'removed' || type == 'updated') {
    _requestProgressiveSync(source: source);
    return true;
  }

  return true;
}


  // ------------------------------------------------------------
  // lifecycle
  // ------------------------------------------------------------
Future<void> onAppStart({
  required CollectionSource source,
  bool resumeBootstrapIfEnabled = true,
}) async {
  final s = await RemoteSettings.load();
  debugPrint('[remote-debug] onAppStart start settings.enabled=${s.enabled} bus=${RemoteSyncBus.instance.stateNotifier.value}');

if (!s.enabled) {
  debugPrint('[remote-debug] remote disabled in settings -> disabling bus and hiding remotes');

  RemoteSyncBus.instance.setDisabled();

  debugPrint(
    '[remote-debug] remote disabled: keeping remote entries in memory, hiding by remoteVisible',
  );

  // Non rimuoviamo remoti dalla RAM.
  // Non facciamo computeFolderMap qui: addEntries/init lo fa già.
  source.invalidateEntries();

  _notifyRemoteVisibilityChanged(source);

  await _stopWs(source);

  debugPrint(
    '[remote-debug] onAppStart end (remote disabled) '
    'bus=${RemoteSyncBus.instance.stateNotifier.value}',
  );

  return;
}

await _waitSourceReady(
  source,
  reason: 'onAppStart-remote-enabled',
);

/*
  final done = await bootstrapDone();
  debugPrint('[remote-debug] bootstrapDone=$done');
*/

var done = await bootstrapDone();

final cachedRemotes = await localMediaDb.loadEntries(
  origin: RemoteOrigin.value,
);

final hasCachedRemotes = cachedRemotes.any((entry) => !entry.trashed);

debugPrint(
  '[remote-debug] bootstrapDone=$done '
  'cachedRemotes=${cachedRemotes.length} '
  'hasCachedRemotes=$hasCachedRemotes',
);

// Se il flag bootstrap non esiste ma abbiamo già remoti nel DB,
// consideriamo il bootstrap già fatto.
// Questo evita fullSync/bootstrap inutili quando riattivi remote.
if (!done && hasCachedRemotes) {
  debugPrint(
    '[remote-debug] bootstrap flag missing but remote cache exists '
    '-> mark bootstrap done and skip full bootstrap',
  );

  await _setBootstrapDone();
  done = true;
}










if (!done) {
  debugPrint(
    '[remote_controller] first bootstrap start '
    'bus=${RemoteSyncBus.instance.stateNotifier.value}',
  );

  RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.syncing;
  RemoteSyncBus.instance.progressNotifier.value = const RemoteSyncProgress(
    done: 0,
    total: 0,
    showOverlay: false,
  );

  _notifyRemoteVisibilityChanged(source);

  debugPrint(
    '[remote_controller] first bootstrap set syncing '
    'bus=${RemoteSyncBus.instance.stateNotifier.value}',
  );

  try {
    debugPrint('[remote_controller] first bootstrap -> ensure stack');
    await _ensureRealtimeStack(source);

    debugPrint('[remote_controller] first bootstrap -> fullSyncImpl');
    await _engine!.fullSyncImpl();

    try {
      await _setBootstrapDone();
      debugPrint('[remote_controller] bootstrap complete: flag set');
    } catch (e, st) {
      debugPrint('[remote_controller] _setBootstrapDone failed: $e\n$st');
    }

    // Dopo il primo bootstrap, carica subito i remoti dal DB nella source.
    // debugPrint('[remote_controller] first bootstrap -> append remotes from DB');
    // await source.appendRemoteEntriesFromDb();



// Aggiorna visibilità/filter senza forzare refresh pesante thumbnails.
_notifyRemoteVisibilityChanged(source);

// Avvia realtime WS.
// NON impostare upToDate qui.
// I remoti diventano visibili solo dopo auth_ok del WS,
// tramite RemoteWsClient.onReady().
debugPrint('[remote_controller] first bootstrap -> start WS');

try {
  await _startWsAlreadyEnsured();

  debugPrint(
    '[remote_controller] first bootstrap WS start requested; '
    'waiting auth_ok before upToDate',
  );
} catch (e, st) {
  debugPrint('[remote_controller] first bootstrap WS start failed: $e\n$st');

  RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown;
  RemoteSyncBus.instance.progressNotifier.value = null;

  source.invalidateEntries();
  _notifyRemoteVisibilityChanged(source);

  rethrow;
}

debugPrint(
  '[remote_controller] first bootstrap end, waiting WS auth_ok '
  'bus=${RemoteSyncBus.instance.stateNotifier.value}',
);
// NON partire subito con progressive sync dopo full bootstrap.
// Il bootstrap ha appena allineato tutto.
// La progressive partirà da WS/eventi o resume.



  } catch (e, st) {
    debugPrint('[remote_controller] first bootstrap failed: $e\n$st');

    RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown;
    RemoteSyncBus.instance.progressNotifier.value = null;

    _notifyRemoteVisibilityChanged(source);
    _forceCollectionRefresh(source);

    rethrow;
  }

  return;
}










  // prima di append: syncing + refresh
  RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.syncing;
  RemoteSyncBus.instance.progressNotifier.value =
      const RemoteSyncProgress(done: 0, total: 0, showOverlay: false);
  _notifyRemoteVisibilityChanged(source);
  debugPrint('[remote-debug] set state=syncing before append');

  // append remoti dal DB (batching nella collection)
  debugPrint('[remote-debug] calling appendRemoteEntriesFromDb()');
  await source.appendRemoteEntriesFromDb();
  debugPrint('[remote-debug] after append state=${RemoteSyncBus.instance.stateNotifier.value} progress=${RemoteSyncBus.instance.progressNotifier.value}');

  // forza refresh Home (cache/lens)
  _notifyRemoteVisibilityChanged(source);
  debugPrint('[remote-debug] notified visibility after append');

// Avvia lo stack realtime.
// Dopo appendRemoteEntriesFromDb(), i remoti sono in RAM/DB,
// ma restano nascosti finché il WS non conferma auth_ok.
debugPrint('[remote-debug] ensuring realtime stack');
await _ensureRealtimeStack(source);

debugPrint('[remote-debug] ensureRealtimeStack done, starting WS');

try {
  await _startWsAlreadyEnsured();

  debugPrint(
    '[remote-debug] WS start requested '
    'ws client present=${_ws != null} '
    'wsConnected=${_ws?.isConnected}',
  );

  // IMPORTANTE:
  // NON impostare upToDate qui.
  // _startWsAlreadyEnsured() avvia la connessione e manda auth,
  // ma auth_ok arriverà asincrono dallo stream WS.
  //
  // RemoteSyncState.upToDate verrà impostato solo da:
  // RemoteWsClient.onReady()
  debugPrint('[remote-debug] waiting WS auth_ok before showing remotes');
} catch (e, st) {
  debugPrint('[remote-debug] WS start failed -> serverDown: $e\n$st');

  RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown;
  RemoteSyncBus.instance.progressNotifier.value = null;

  source.invalidateEntries();
  _notifyRemoteVisibilityChanged(source);

  return;
}

debugPrint('[remote-debug] exiting onAppStart without immediate progressive sync');

}


  Future<void> toggleRemote({required CollectionSource source}) async {
    final s = await RemoteSettings.load();

    debugPrint(
      '[remote_controller] toggleRemote start '
      'enabledBefore=${s.enabled} '
      'busBefore=${RemoteSyncBus.instance.stateNotifier.value}',
    );


    final upd = RemoteSettings(
      enabled: !s.enabled,
      baseUrl: s.baseUrl,
      indexPath: s.indexPath,
      email: s.email,
      password: s.password,
      wsUrl: s.wsUrl,
    );
    await upd.save();

    debugPrint(
      '[remote_controller] toggleRemote saved '
      'enabledAfter=${upd.enabled} '
      'bus=${RemoteSyncBus.instance.stateNotifier.value}',
    );

if (!upd.enabled) {
  debugPrint(
    '[remote_controller] disabling remote '
    'busBeforeDisable=${RemoteSyncBus.instance.stateNotifier.value}',
  );

  RemoteSyncBus.instance.setDisabled();

  debugPrint(
    '[remote_controller] disabled remote '
    'busAfterDisable=${RemoteSyncBus.instance.stateNotifier.value}',
  );

  debugPrint(
    '[remote_controller] disabling remote: keeping remote entries in memory, hiding by remoteVisible',
  );

  source.invalidateEntries();
  //await source.computeFolderMap(reason: 'remote-disabled-toggle');
  //await source.updateDirectories();

  _notifyRemoteVisibilityChanged(source);

  await _stopWs(source);
  return;
}

    // ------------------------------------------------------------
    // PATCH 2: se è la prima volta, mostra overlay (barra progresso) al bootstrap
    // ------------------------------------------------------------
    final first = !(await bootstrapDone());


debugPrint(
  '[remote_controller] toggleRemote enabling '
  'first=$first '
  'busBeforeOnAppStart=${RemoteSyncBus.instance.stateNotifier.value}',
);

    await onAppStart(source: source, resumeBootstrapIfEnabled: first);


debugPrint(
  '[remote_controller] toggleRemote after onAppStart '
  'busAfterOnAppStart=${RemoteSyncBus.instance.stateNotifier.value}',
);


  }

  // ------------------------------------------------------------
  // full sync legacy (progress + overlay)
  // ------------------------------------------------------------
Future<void> fullSync({
  required CollectionSource source,
  required bool showOverlay,
  bool markBootstrapDoneOnSuccess = false,
}) async {
  if (_syncInFlight) return;
  _syncInFlight = true;

  final s = await RemoteSettings.load();
  if (!s.enabled) {
    RemoteSyncBus.instance.setDisabled();
    _notifyRemoteVisibilityChanged(source);
    _forceCollectionRefresh(source);
    _syncInFlight = false;
    return;
  }

  try {
    if (s.baseUrl.trim().isEmpty) {
      RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown;
      RemoteSyncBus.instance.progressNotifier.value = null;
      _notifyRemoteVisibilityChanged(source);
      _forceCollectionRefresh(source);
      _syncInFlight = false;
      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);
    await client.ping().timeout(const Duration(seconds: 3));

    final items = await client.fetchAll().timeout(const Duration(seconds: 30));
    final total = items.length;

    final opId = RemoteSyncBus.instance.start(total: total, showOverlay: showOverlay);
    _notifyRemoteVisibilityChanged(source);

    final repo = RemoteRepository(localMediaDb.rawDb);

    // Decide se è bootstrap (solo in quel caso deleteAllRemotes)
    final isBootstrap = !(await bootstrapDone());
    final serverIds = items.map((e) => e.id).where((v) => v.isNotEmpty).toSet();

    if (isBootstrap) {
      debugPrint('[remote-debug] fullSync bootstrap: deleteAllRemotes + upsert chunked');
      await repo.deleteAllRemotes();
      // upsert in chunk più piccoli per non bloccare il main thread
      const chunk = 40;
      int done = 0;
      for (var i = 0; i < total; i += chunk) {
        final end = (i + chunk < total) ? i + chunk : total;
        await repo.upsertAll(items.sublist(i, end), chunkSize: chunk);
        done = end;
        RemoteSyncBus.instance.update(opId: opId, done: done, total: total);
      }
      try {
        await _setBootstrapDone();
        debugPrint('[remote-debug] bootstrap flag set after fullSync');
      } catch (e, st) {
        debugPrint('[remote-debug] _setBootstrapDone failed: $e\n$st');
      }
    } else {
      debugPrint('[remote-debug] fullSync incremental: upsert chunked + pruneMissingRemotes');
      const chunk = 40;
      int done = 0;
      for (var i = 0; i < total; i += chunk) {
        final end = (i + chunk < total) ? i + chunk : total;
        await repo.upsertAll(items.sublist(i, end), chunkSize: chunk);
        done = end;
        RemoteSyncBus.instance.update(opId: opId, done: done, total: total);
      }
      await repo.pruneMissingRemotes(serverIds);
    }

    // append remoti dal DB (collection batching)
    await source.appendRemoteEntriesFromDb();

    _notifyRemoteVisibilityChanged(source);

// Non impostare upToDate qui.
// Dopo il fullSync i dati sono nel DB/source, ma i remoti devono diventare
// visibili solo dopo auth_ok del WebSocket, tramite RemoteWsClient.onReady().
RemoteSyncBus.instance.progressNotifier.value = null;
_notifyRemoteVisibilityChanged(source);

// Assicurati che lo stack realtime sia avviato e poi avvia WS.
// onReady() imposterà upToDate dopo auth_ok.
await _ensureRealtimeStack(source);

try {
  await _startWsAlreadyEnsured();

  debugPrint(
    '[remote] fullSync completed, WS start requested; '
    'waiting auth_ok before showing remotes',
  );
} catch (e, st) {
  debugPrint('[remote] fullSync WS start failed: $e\n$st');

  RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown;
  RemoteSyncBus.instance.progressNotifier.value = null;

  source.invalidateEntries();
  _notifyRemoteVisibilityChanged(source);
}

  } catch (e, st) {
    debugPrint('[remote] fullSync error: $e\n$st');
    RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown;
    RemoteSyncBus.instance.progressNotifier.value = null;
    _notifyRemoteVisibilityChanged(source);
    _forceCollectionRefresh(source);

    _retryStartTime ??= DateTime.now();
    _scheduleRetry(source);
  } finally {
    _syncInFlight = false;
  }
}

  // ------------------------------------------------------------
  // retry
  // ------------------------------------------------------------
  void _scheduleRetry(CollectionSource source) {
    _retryTimer?.cancel();

    _retryTimer = Timer(const Duration(seconds: 30), () async {
      final s = await RemoteSettings.load();
      if (!s.enabled) return;

      if (_retryStartTime != null) {
        final elapsed = DateTime.now().difference(_retryStartTime!);
        if (elapsed > const Duration(minutes: 5)) {
          debugPrint('[remote] retry timeout -> disattivo remote');

debugPrint('[remote] retry timeout: keeping remote entries in memory, hiding by remoteVisible');

source.invalidateEntries();
// await source.computeFolderMap(reason: 'remote-disabled-retry-timeout');
// await source.updateDirectories();
_notifyRemoteVisibilityChanged(source);

          final upd = RemoteSettings(
            enabled: false,
            baseUrl: s.baseUrl,
            indexPath: s.indexPath,
            email: s.email,
            password: s.password,
            wsUrl: s.wsUrl,
          );
          await upd.save();

          RemoteSyncBus.instance.setDisabled();
          _notifyRemoteVisibilityChanged(source);
          //_forceCollectionRefresh(source);

          await _stopWs(source);
          return;
        }
      }

      debugPrint('[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));
        debugPrint('[remote] retry OK -> riprendo sync');

        _retryTimer = null;
        _retryStartTime = null;

        unawaited(fullSync(source: source, showOverlay: false));
      } catch (_) {
        debugPrint('[remote] retry fallito');
        _scheduleRetry(source);
      }
    });
  }

Future<void> onResume(CollectionSource source) async {
  final s = await RemoteSettings.load();
  if (!s.enabled) return;

  await _ensureRealtimeStack(source);

  try {
    await _startWsAlreadyEnsured();

    debugPrint(
      '[remote] onResume WS start requested; '
      'waiting auth_ok before showing remotes',
    );
  } catch (e, st) {
    debugPrint('[remote] onResume WS start failed: $e\n$st');

    RemoteSyncBus.instance.stateNotifier.value = RemoteSyncState.serverDown;
    RemoteSyncBus.instance.progressNotifier.value = null;

    source.invalidateEntries();
    _notifyRemoteVisibilityChanged(source);
  }

  // Non schedulare progressive qui.
  // Se serve recovery, sarà il WS/server a chiederla con need_recovery.
}

}
