// Maps incoming LiveEvent kinds to provider invalidations.
//
// Activated by ref.read(liveEventsDispatcherProvider) in app.dart; once
// activated, the dispatcher listens to liveEventsProvider for the
// lifetime of the ProviderScope and invalidates the small set of
// public-scoped providers we know about.
//
// Screen-scoped providers (file-private providers in library_screen.dart,
// admin handlers, etc.) opt in to live-refresh by themselves listening
// to liveEventsProvider — the dispatcher only handles cross-screen,
// publicly-importable providers. This keeps the dispatcher small and
// avoids a back-edge dependency from /shared onto every feature folder.
//
// Includes an AppLifecycleState resume handler that defensively
// invalidates the same set on app foreground. SSE will catch up on its
// own, but on cold-start or after a long background period the
// re-invalidate is fast and avoids stale data flashing before the
// stream reconnects.

import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../library/library_providers.dart' show homeIndexProvider;
import '../quarantine/quarantine_provider.dart';
import 'live_events_provider.dart';

/// Activates the SSE → invalidation dispatcher for the lifetime of the
/// containing ProviderScope. Read this once at startup (from app.dart);
/// the provider's body runs once, sets up listeners, and returns a
/// sentinel value.
final liveEventsDispatcherProvider = Provider<_LiveEventsDispatcher>((ref) {
  final d = _LiveEventsDispatcher(ref);
  ref.onDispose(d.dispose);
  return d;
});

class _LiveEventsDispatcher with WidgetsBindingObserver {
  _LiveEventsDispatcher(this._ref) {
    // Subscribe to the event stream. Each emitted event invokes
    // _handle. Errors / disconnects auto-rebuild the underlying
    // provider; we don't need to react to them here.
    _sub = _ref.listen<AsyncValue<LiveEvent>>(
      liveEventsProvider,
      (_, next) => next.whenData(_handle),
      fireImmediately: false,
    );
    WidgetsBinding.instance.addObserver(this);
  }

  final Ref _ref;
  late final ProviderSubscription<AsyncValue<LiveEvent>> _sub;

  void _handle(LiveEvent e) {
    // Map event kinds to provider invalidations. Keep this list short:
    // only providers reachable from /shared. Screen-private providers
    // listen to liveEventsProvider themselves.
    switch (e.kind) {
      case 'quarantine.flagged':
      case 'quarantine.unflagged':
      case 'quarantine.resolved':
      case 'quarantine.file_deleted':
      case 'quarantine.deleted_via_lidarr':
        _ref.invalidate(myQuarantineProvider);
        // Hidden / liked / album rows referencing a deleted track may
        // now be stale — homeIndexProvider re-fetch refreshes the cards.
        _ref.invalidate(homeIndexProvider);
      case 'playlist.created':
      case 'playlist.updated':
      case 'playlist.deleted':
      case 'playlist.tracks_changed':
        // Home renders a Playlists row; refresh it so a delete or add
        // is reflected immediately. Detail screens that need the
        // mutated row will get a separate invalidate from their own
        // listener (filed as a follow-up).
        _ref.invalidate(homeIndexProvider);
      case 'scan.run_started':
      case 'scan.run_finished':
        // Admin scan card provider lives in the admin feature folder
        // and is file-private today; will be invalidated by the admin
        // dashboard's own listener once that exists.
        break;
      default:
        // track.liked / track.unliked / request.status_changed reach
        // screen-private providers; their screens listen directly.
        break;
    }
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      // Defensive cold-start invalidation. SSE will catch up on its
      // own, but the re-invalidate flushes any stale data that
      // landed while the app was backgrounded.
      _ref.invalidate(myQuarantineProvider);
      _ref.invalidate(homeIndexProvider);
    }
  }

  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _sub.close();
  }
}
