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

import 'alt_socket_client.dart';
import 'socket_endpoint.dart';

enum SocketConnectionStatus {
  disconnected,
  connecting,
  connected,
  error,
  timeout,
}

class SocketConnectionState {
  final SocketConnectionStatus status;
  final String? reasonCode;
  final AltSocketEndpoint? endpoint;
  final Object? lastError;

  const SocketConnectionState({
    required this.status,
    this.reasonCode,
    this.endpoint,
    this.lastError,
  });

  static const disconnected = SocketConnectionState(
    status: SocketConnectionStatus.disconnected,
  );
  static const connecting = SocketConnectionState(
    status: SocketConnectionStatus.connecting,
  );
  static const connected = SocketConnectionState(
    status: SocketConnectionStatus.connected,
  );
  static const error = SocketConnectionState(
    status: SocketConnectionStatus.error,
  );
  static const timeout = SocketConnectionState(
    status: SocketConnectionStatus.timeout,
  );

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is SocketConnectionState &&
          status == other.status &&
          reasonCode == other.reasonCode &&
          endpoint == other.endpoint &&
          lastError == other.lastError;

  @override
  int get hashCode => Object.hash(status, reasonCode, endpoint, lastError);

  @override
  String toString() =>
      'SocketConnectionState(status: $status, reasonCode: $reasonCode, endpoint: $endpoint, lastError: $lastError)';
}

class SocketConnectionController extends Notifier<SocketConnectionState> {
  AltSocketClient? _client;

  @visibleForTesting
  Future<AltSocketClient> Function(AltSocketEndpoint)? connectFactory;

  @override
  SocketConnectionState build() {
    ref.onDispose(() {
      final clientToClose = _client;
      _client = null;
      clientToClose?.close();
    });
    return SocketConnectionState.disconnected;
  }

  AltSocketClient? get client => _client;

  Future<void> connect(AltSocketEndpoint endpoint) async {
    if (state.status == SocketConnectionStatus.connecting ||
        state.status == SocketConnectionStatus.connected) {
      return;
    }

    state = SocketConnectionState(
      status: SocketConnectionStatus.connecting,
      endpoint: endpoint,
    );

    AltSocketClient? newClient;
    try {
      final oldClient = _client;
      _client = null;
      await oldClient?.close();
      newClient = connectFactory != null
          ? await connectFactory!(endpoint)
          : await AltSocketClient.connect(endpoint);
      _client = newClient;

      // Perform handshake/hello
      await newClient.hello();

      state = SocketConnectionState(
        status: SocketConnectionStatus.connected,
        endpoint: endpoint,
      );

      // Listen to disconnect events
      newClient.addDisconnectListener((_) {
        if (_client == newClient) {
          state = SocketConnectionState(
            status: SocketConnectionStatus.disconnected,
            endpoint: endpoint,
            reasonCode: 'socket_closed',
          );
        }
      });
    } catch (e) {
      final isTimeout =
          e is TimeoutException ||
          e.toString().toLowerCase().contains('timeout');
      state = SocketConnectionState(
        status: isTimeout
            ? SocketConnectionStatus.timeout
            : SocketConnectionStatus.error,
        endpoint: endpoint,
        reasonCode: newClient == null
            ? (isTimeout ? 'connect_timeout' : 'connect_failed')
            : (isTimeout ? 'hello_timeout' : 'hello_failed'),
        lastError: e,
      );
      rethrow;
    }
  }

  Future<void> disconnect() async {
    final prevEndpoint = state.endpoint;
    final clientToClose = _client;
    _client = null;
    state = SocketConnectionState(
      status: SocketConnectionStatus.disconnected,
      endpoint: prevEndpoint,
      reasonCode: 'explicit_disconnect',
    );
    await clientToClose?.close();
  }

  @visibleForTesting
  set debugState(SocketConnectionState value) {
    state = value;
  }
}

final socketConnectionControllerProvider =
    NotifierProvider<SocketConnectionController, SocketConnectionState>(
      SocketConnectionController.new,
    );
