import 'dart:async';

import 'package:connectivity_plus/connectivity_plus.dart';

import 'social_service.dart';
import 'social_settings.dart';

/// Opens a session against the given relays. Injectable so the connection logic
/// can be unit-tested without a real relay.
typedef SessionOpener = Future<SocialSession> Function(List<String> relays);

/// ONE shared relay connection per identity, reused by every social feature
/// (offers, messaging, trust, profile, the inbox listener) instead of each
/// opening its own `RelayPool`. Fewer sockets, less battery, one place to manage
/// reconnection.
///
/// Lazily connects on first [session] call; reconnects when the network returns
/// and drops the session when it goes away, announcing each change on [sessions]
/// so long-lived consumers (the inbox listener) can re-subscribe. Callers must
/// NOT close the session they get — this owns its lifecycle. Recreated on an
/// identity switch (the old one is disposed).
class SocialConnection {
  SocialConnection({
    required SocialService social,
    required SocialSettings settings,
    SessionOpener? open,
    Stream<bool>? online,
  })  : _settings = settings,
        _open = open ?? social.openSession,
        _online = online;

  final SocialSettings _settings;
  final SessionOpener _open;
  final Stream<bool>? _online;

  final _sessions = StreamController<SocialSession?>.broadcast();
  SocialSession? _current;
  Future<SocialSession?>? _pending;
  StreamSubscription<bool>? _onlineSub;
  bool _disposed = false;

  /// Emits the live session on each (re)connect, and null when it drops.
  Stream<SocialSession?> get sessions => _sessions.stream;

  /// The current shared session, or null if not connected right now.
  SocialSession? get current => _current;

  /// Begins watching connectivity (reconnect on regain, drop when offline) and
  /// attempts an initial connect. Idempotent-ish; call once at startup.
  void start() {
    _onlineSub = (_online ?? _connectivityOnline()).listen((isOnline) {
      if (!isOnline) {
        _drop();
      } else if (_current == null) {
        unawaited(session());
      }
    });
    unawaited(session()); // initial attempt (also connects if already online)
  }

  /// The shared session, connecting on first use. Returns null when offline or
  /// no relay is reachable; the feature then degrades gracefully.
  Future<SocialSession?> session() {
    if (_current != null) return Future.value(_current);
    return _pending ??= _connect();
  }

  Future<SocialSession?> _connect() async {
    try {
      final relays = await _settings.relayUrls();
      if (relays.isEmpty) return null;
      final s = await _open(relays);
      if (_disposed) {
        await s.close();
        return null;
      }
      _current = s;
      _sessions.add(s);
      return s;
    } catch (_) {
      return null; // unreachable — a later connectivity change retries
    } finally {
      _pending = null;
    }
  }

  void _drop() {
    final s = _current;
    _current = null;
    if (s != null) {
      unawaited(s.close());
      if (!_sessions.isClosed) _sessions.add(null);
    }
  }

  Future<void> dispose() async {
    _disposed = true;
    await _onlineSub?.cancel();
    _onlineSub = null;
    _drop();
    if (!_sessions.isClosed) await _sessions.close();
  }

  static Stream<bool> _connectivityOnline() =>
      Connectivity().onConnectivityChanged.map((results) =>
          results.any((r) => r != ConnectivityResult.none));
}
