import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import '../mixins/disposable_change_notifier_mixin.dart';
import 'multi_server_provider.dart';
import '../services/multi_server_manager.dart';
import '../services/offline_mode_source.dart';

enum OfflineModeReason {
  online,
  noNetworkConnection,
  waitingForServerStatus,
  noKnownVisibleServers,
  onlyAuthErrorServers,
  noServerConnection,
}

/// Tracks offline mode status based on network connectivity and server reachability.
class OfflineModeProvider extends ChangeNotifier with DisposableChangeNotifierMixin implements OfflineModeSource {
  final MultiServerManager _serverManager;
  MultiServerProvider? _multiServerProvider;

  StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
  StreamSubscription<Map<String, bool>>? _serverStatusSubscription;

  bool _hasNetworkConnection = true;
  late bool _hasServerConnection;
  bool _lastOfflineState = false;
  bool _isInitialized = false;

  /// Latest raw connectivity results. This provider owns the app's single
  /// `Connectivity()` subscription; consumers needing the connection *type*
  /// (e.g. the WiFi-reconnect sync trigger in main.dart) read it from here
  /// instead of subscribing themselves.
  List<ConnectivityResult> _lastConnectivityResults = const [];
  bool _lastWifiOrEthernetState = false;

  /// Whether the current connection is WiFi or Ethernet (unmetered-ish).
  bool get hasWifiOrEthernet =>
      _lastConnectivityResults.contains(ConnectivityResult.wifi) ||
      _lastConnectivityResults.contains(ConnectivityResult.ethernet);

  /// True once [MultiServerManager] has emitted its first server-status
  /// snapshot. Until then we don't actually know whether any server is
  /// online — the binder hasn't finished its first connect yet — so we
  /// treat the app as online to avoid flashing the "offline" UI for the
  /// few hundred ms it takes to come up. After the first emission we
  /// trust the real flag.
  bool _hasReceivedServerStatus = false;

  OfflineModeProvider(this._serverManager, {MultiServerProvider? multiServerProvider})
    : _multiServerProvider = multiServerProvider,
      _hasServerConnection = (multiServerProvider?.hasConnectedServers ?? _serverManager.onlineServerIds.isNotEmpty) {
    // Pre-seed the "received status" flag if there are already online
    // servers (e.g. provider rebuilt mid-session) or the active profile's
    // visibility filter has already settled.
    _markServerStatusKnownIfSettled();
    _lastOfflineState = isOffline;
    _multiServerProvider?.addListener(_handleMultiServerProviderChanged);
  }

  /// Whether the app is currently in offline mode
  /// Offline = no network OR (we know servers are unreachable)
  @override
  bool get isOffline =>
      offlineReason == OfflineModeReason.noNetworkConnection || offlineReason == OfflineModeReason.noServerConnection;

  OfflineModeReason get offlineReason {
    if (!_hasNetworkConnection) return OfflineModeReason.noNetworkConnection;
    if (!_hasReceivedServerStatus) return OfflineModeReason.waitingForServerStatus;
    if (!_hasKnownVisibleServers) return OfflineModeReason.noKnownVisibleServers;
    if (_hasOnlyAuthErrorServers) return OfflineModeReason.onlyAuthErrorServers;
    if (!_hasServerConnection) return OfflineModeReason.noServerConnection;
    return OfflineModeReason.online;
  }

  /// Whether there is network connectivity (WiFi, mobile data, etc.)
  @visibleForTesting
  bool get hasNetworkConnection => _hasNetworkConnection;

  /// Whether at least one media server (Plex or Jellyfin) is reachable
  @visibleForTesting
  bool get hasServerConnection => _hasServerConnection;

  bool get _hasKnownVisibleServers =>
      (_multiServerProvider?.expectedServerIds.length ?? _serverManager.serverIds.length) > 0;

  bool get _hasOnlyAuthErrorServers {
    final provider = _multiServerProvider;
    if (provider == null) return false;
    final serverCount = provider.expectedServerIds.length;
    return serverCount > 0 && provider.authErrorServerIds.length == serverCount;
  }

  /// Attach the profile-visible server provider. Offline state is evaluated
  /// against visible servers, not global manager state, so another profile's
  /// online server does not keep the active profile out of offline mode.
  void updateMultiServerProvider(MultiServerProvider provider) {
    if (identical(_multiServerProvider, provider)) return;
    _multiServerProvider?.removeListener(_handleMultiServerProviderChanged);
    _multiServerProvider = provider;
    _multiServerProvider?.addListener(_handleMultiServerProviderChanged);
    _hasServerConnection = provider.hasConnectedServers;
    _markServerStatusKnownIfSettled();
    _notifyIfOfflineChanged();
  }

  /// Updates network and server connection flags
  Future<void> _updateConnectionFlags() async {
    try {
      final connectivityResult = await Connectivity().checkConnectivity().timeout(
        const Duration(seconds: 3),
        onTimeout: () => [ConnectivityResult.other],
      );
      _lastConnectivityResults = connectivityResult;
      _lastWifiOrEthernetState = hasWifiOrEthernet;
      _hasNetworkConnection = !connectivityResult.contains(ConnectivityResult.none);
    } catch (e) {
      // connectivity_plus can throw PlatformException on Windows (NetworkManager::StartListen)
      _hasNetworkConnection = true;
    }
    _hasServerConnection = _multiServerProvider?.hasConnectedServers ?? _serverManager.onlineServerIds.isNotEmpty;
  }

  void _handleMultiServerProviderChanged() {
    _hasServerConnection = _multiServerProvider?.hasConnectedServers ?? _serverManager.onlineServerIds.isNotEmpty;
    _markServerStatusKnownIfSettled();
    _notifyIfOfflineChanged();
  }

  void _markServerStatusKnownIfSettled() {
    if (_hasServerConnection || (_multiServerProvider?.hasExplicitVisibleServerFilter ?? false)) {
      _hasReceivedServerStatus = true;
    }
  }

  void _notifyIfOfflineChanged() {
    final offline = isOffline;
    if (_lastOfflineState == offline) return;
    _lastOfflineState = offline;
    safeNotifyListeners();
  }

  /// Initialize the provider and start monitoring
  Future<void> initialize() async {
    if (_isInitialized) return;
    _isInitialized = true;

    // Check initial connectivity
    await _updateConnectionFlags();

    // Monitor connectivity changes — runZonedGuarded catches async errors from
    // connectivity_plus (e.g. DBusServiceUnknownException on Linux without NetworkManager)
    runZonedGuarded(
      () {
        _connectivitySubscription = Connectivity().onConnectivityChanged.listen(
          (results) {
            _lastConnectivityResults = results;
            _hasNetworkConnection = !results.contains(ConnectivityResult.none);
            // Notify on connection-type changes too (WiFi <-> cellular), not
            // just offline flips — type consumers listen through this provider.
            final wifiNow = hasWifiOrEthernet;
            if (wifiNow != _lastWifiOrEthernetState) {
              _lastWifiOrEthernetState = wifiNow;
              _lastOfflineState = isOffline;
              safeNotifyListeners();
            } else {
              _notifyIfOfflineChanged();
            }
          },
          onError: (e) {
            _hasNetworkConnection = true;
          },
        );
      },
      (error, stack) {
        // connectivity_plus throws DBusServiceUnknownException on Linux without NetworkManager
        _hasNetworkConnection = true;
      },
    );

    // Monitor server status from MultiServerManager
    _serverStatusSubscription = _serverManager.statusStream.listen((statusMap) {
      _hasServerConnection = _multiServerProvider?.hasConnectedServers ?? statusMap.values.any((isOnline) => isOnline);
      _hasReceivedServerStatus = true;
      _notifyIfOfflineChanged();
    });

    _lastOfflineState = isOffline;
    safeNotifyListeners();
  }

  /// Force a refresh of connectivity status
  Future<void> refresh() async {
    await _updateConnectionFlags();
    _lastOfflineState = isOffline;
    safeNotifyListeners();
  }

  @override
  void dispose() {
    _multiServerProvider?.removeListener(_handleMultiServerProviderChanged);
    _connectivitySubscription?.cancel();
    _serverStatusSubscription?.cancel();
    super.dispose();
  }
}
