import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import 'stream_recovery.dart';
import 'supabase_provider.dart';

final realtimeControllerProvider = ChangeNotifierProvider<RealtimeController>((
  ref,
) {
  final client = ref.watch(supabaseClientProvider);
  final controller = RealtimeController(client);
  ref.onDispose(controller.dispose);
  return controller;
});

/// Per-channel realtime controller for UI skeleton indicators.
///
/// Individual streams handle their own recovery via [StreamRecoveryWrapper].
/// This controller aggregates channel-level status so the UI can show
/// per-channel skeleton shimmers (e.g. only the tasks list shimmers when
/// the `tasks` channel is recovering, not the whole app).
///
/// Coordinates:
/// - Per-channel recovering state for pinpoint skeleton indicators
/// - Auth token refreshes for realtime connections
class RealtimeController extends ChangeNotifier {
  final SupabaseClient _client;
  bool _disposed = false;

  /// Channels currently in a recovering/polling/stale state.
  final Set<String> _recoveringChannels = {};

  RealtimeController(this._client) {
    _init();
  }

  void _init() {
    try {
      _client.auth.onAuthStateChange.listen((data) {
        final event = data.event;
        if (event == AuthChangeEvent.tokenRefreshed) {
          _ensureTokenFresh();
        }
      });
    } catch (e) {
      debugPrint('RealtimeController._init error: $e');
    }
  }

  Future<void> _ensureTokenFresh() async {
    if (_disposed) return;
    try {
      final authDynamic = _client.auth as dynamic;
      if (authDynamic.refreshSession != null) {
        await authDynamic.refreshSession?.call();
      }
    } catch (e) {
      debugPrint('RealtimeController: token refresh failed: $e');
    }
  }

  // ── Per-channel status ─────────────────────────────────────────────────

  /// Whether a specific channel is currently recovering.
  bool isChannelRecovering(String channel) =>
      _recoveringChannels.contains(channel);

  /// Global flag: true if **any** channel is recovering. Useful for global
  /// indicators (e.g. dashboard) where per-channel granularity isn't needed.
  bool get isAnyStreamRecovering => _recoveringChannels.isNotEmpty;

  /// The set of channels currently recovering, for UI display.
  Set<String> get recoveringChannels => Set.unmodifiable(_recoveringChannels);

  /// Mark a channel as recovering. Called by [StreamRecoveryWrapper] via its
  /// [ChannelStatusCallback].
  void markChannelRecovering(String channel) {
    if (_recoveringChannels.add(channel)) {
      notifyListeners();
    }
  }

  /// Mark a channel as recovered. Called when realtime reconnects
  /// successfully.
  void markChannelRecovered(String channel) {
    if (_recoveringChannels.remove(channel)) {
      notifyListeners();
    }
  }

  /// Convenience callback suitable for [StreamRecoveryWrapper.onStatusChanged].
  ///
  /// Routes [StreamConnectionStatus] to the appropriate mark method.
  /// Both `connected` and `polling` are treated as "recovered" because
  /// polling is a functional fallback that still delivers data — the user
  /// doesn't need to see a reconnection indicator while data flows via REST.
  void handleChannelStatus(String channel, StreamConnectionStatus status) {
    if (status == StreamConnectionStatus.connected ||
        status == StreamConnectionStatus.polling) {
      markChannelRecovered(channel);
    } else {
      markChannelRecovering(channel);
    }
  }

  // ── Legacy compat ─────────────────────────────────────────────────────

  /// @deprecated Use [markChannelRecovering] instead.
  void markStreamRecovering() {
    // Kept for backward compatibility; maps to a synthetic channel.
    markChannelRecovering('_global');
  }

  /// @deprecated Use [markChannelRecovered] instead.
  void markStreamRecovered() {
    markChannelRecovered('_global');
  }

  @override
  void dispose() {
    _disposed = true;
    super.dispose();
  }
}
