import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../config/app_config.dart';

class WebSocketClient {
  WebSocketChannel? _channel;
  StreamSubscription? _streamSubscription;
  final String baseUrl;
  Timer? _heartbeatTimer;
  Timer? _reconnectTimer;
  bool _isConnected = false;
  int _reconnectAttempts = 0;
  String? _lastPath;
  String? _lastToken;
  final _messageController = StreamController<Map<String, dynamic>>.broadcast();

  static const int _maxReconnectAttempts = 10;
  static const Duration _baseReconnectDelay = Duration(milliseconds: 1000);
  static const Duration _maxReconnectDelay = Duration(seconds: 60);

  Stream<Map<String, dynamic>> get messages => _messageController.stream;
  bool get isConnected => _isConnected;

  WebSocketClient({required this.baseUrl});

  Future<void> connect(String path, {String? token}) async {
    _lastPath = path;
    _lastToken = token;

    // Tear down previous connection to prevent duplicate event delivery.
    // 1) Cancel the stream subscription first — stops events from reaching
    //    _messageController immediately (no more duplicates).
    // 2) Fire-and-forget sink.close() — don't await because
    //    IOWebSocketChannel.sink.close() can hang indefinitely
    //    (known issue: https://github.com/dart-lang/web_socket_channel/issues/185).
    await _closeCurrentConnection();

    final uri = Uri.parse('$baseUrl$path');
    try {
      _channel = IOWebSocketChannel.connect(
        uri,
        headers: token != null ? {'Authorization': 'Bearer $token'} : null,
      );
      _isConnected = true;
      _reconnectAttempts = 0;

      _streamSubscription = _channel!.stream.listen(
        (data) {
          final decoded = jsonDecode(data as String) as Map<String, dynamic>;
          _messageController.add(decoded);
        },
        onDone: () {
          _isConnected = false;
          _scheduleReconnect();
        },
        onError: (error) {
          _isConnected = false;
          _scheduleReconnect();
        },
      );

      _startHeartbeat();
    } catch (e) {
      _isConnected = false;
      _scheduleReconnect();
    }
  }

  void send(Map<String, dynamic> data) {
    if (_isConnected && _channel != null) {
      _channel!.sink.add(jsonEncode(data));
    }
  }

  void _startHeartbeat() {
    _heartbeatTimer?.cancel();
    _heartbeatTimer = Timer.periodic(
      const Duration(seconds: 30),
      (_) => send({'type': 'ping'}),
    );
  }

  /// Reconnects with exponential backoff + jitter, up to [_maxReconnectAttempts].
  void _scheduleReconnect() {
    _heartbeatTimer?.cancel();
    _reconnectTimer?.cancel();

    if (_reconnectAttempts >= _maxReconnectAttempts) return;

    // Exponential backoff: 1s, 2s, 4s, 8s, ... capped at 60s
    final delayMs = min(
      _baseReconnectDelay.inMilliseconds * pow(2, _reconnectAttempts).toInt(),
      _maxReconnectDelay.inMilliseconds,
    );
    final jitter = Random().nextInt(500);

    _reconnectAttempts++;
    _reconnectTimer = Timer(
      Duration(milliseconds: delayMs + jitter),
      () {
        if (_lastPath != null) {
          connect(_lastPath!, token: _lastToken);
        }
      },
    );
  }

  /// Resets reconnect counter and forces an immediate reconnect attempt.
  Future<void> reconnect() async {
    _reconnectAttempts = 0;
    if (_lastPath != null) {
      await connect(_lastPath!, token: _lastToken);
    }
  }

  Future<void> disconnect() async {
    _heartbeatTimer?.cancel();
    _reconnectTimer?.cancel();
    _reconnectAttempts = _maxReconnectAttempts; // Prevent auto-reconnect
    await _closeCurrentConnection();
  }

  /// Cancel stream subscription, then fire-and-forget sink.close().
  Future<void> _closeCurrentConnection() async {
    _heartbeatTimer?.cancel();
    _isConnected = false;

    // Cancel subscription first — this immediately stops events from
    // the old channel reaching _messageController.
    if (_streamSubscription != null) {
      await _streamSubscription!.cancel();
      _streamSubscription = null;
    }

    // Close the underlying WebSocket. Don't await — sink.close() can hang.
    if (_channel != null) {
      final old = _channel!;
      _channel = null;
      unawaited(old.sink.close(status.goingAway).catchError((_) {}));
    }
  }

  void dispose() {
    disconnect();
    _messageController.close();
  }
}

final webSocketClientProvider = Provider<WebSocketClient>((ref) {
  final config = ref.watch(appConfigProvider);
  return WebSocketClient(baseUrl: config.wsBaseUrl);
});
