import 'dart:async';
import 'dart:convert';
import 'dart:isolate';

import 'package:backend/game_room_manager.dart';
import 'package:dart_frog_web_socket/dart_frog_web_socket.dart';
import 'package:logging/logging.dart';
import 'package:shared_models/room.dart';

final _logger = Logger('SocketManager');

class SocketManager {
  // Default constructor returns instance
  factory SocketManager() {
    return instance;
  }

  // Private constructor
  SocketManager._();

  // Singleton instance
  static final SocketManager instance = SocketManager._();

  // Store connections: GameRoomUuid -> UserUuid -> WebSocketChannel
  final _connections = <String, Map<String, WebSocketChannel>>{};
  // Store isolate port and stream subscription to said port
  final _gameRoomSendPorts = <String, SendPort>{};
  final _gameRoomPortSubs = <String, StreamSubscription<dynamic>>{};
  final _gameRoomUserWsSubs = <String, StreamSubscription<dynamic>>{};

  // Add a new connection
  Future<Status> addConnection(
    WebSocketChannel connection, {
    required String roomUuid,
    required String userUuid,
  }) async {
    _logger.finer('Adding connection to socket manager for user $userUuid in room $roomUuid');
    if (!_gameRoomPortSubs.containsKey(roomUuid)) {
      final status = await spawnGameRoomIsolate(roomUuid);
      if (status == Status.failure) {
        return status;
      }
    }
    _connections.putIfAbsent(roomUuid, () => <String, WebSocketChannel>{});
    _connections[roomUuid]![userUuid] = connection;
    _logger.fine('Listening to websocket for messages');
    // ignore: cancel_subscriptions
    final sub = connection.stream.listen(_gameRoomMessageListener);
    _gameRoomUserWsSubs[userUuid] = sub;

    return Status.success;
  }

  // Remove a connection
  void removeConnection(String roomUuid, String userUuid) {
    _connections[roomUuid]?.remove(userUuid);
    if (_connections[roomUuid]?.isEmpty ?? false) {
      _connections.remove(roomUuid);
    }
  }

  // Get a specific user's connection
  WebSocketChannel? getConnection(String roomUuid, String userUuid) {
    return _connections[roomUuid]?[userUuid];
  }

  // Get all connections in a room
  Map<String, WebSocketChannel>? getRoomConnections(String roomUuid) {
    return _connections[roomUuid];
  }

  // Broadcast message to all users in a room except sender
  void broadcastToRoom(String roomUuid, GameRoomMessage message, {String? excludeUserUuid}) {
    _logger.fine('Broadcasting ${message.type} to room $roomUuid');
    _connections[roomUuid]?.forEach((userUuid, connection) {
      if (message is PingMessage) {
        message.userUuid = userUuid;
      }
      if (userUuid != excludeUserUuid) {
        connection.sink.add(jsonEncode(message.toJson()));
      }
    });
  }

  // Check if a user is connected
  bool isConnected(String roomUuid, String userUuid) {
    return _connections[roomUuid]?.containsKey(userUuid) ?? false;
  }

  Future<Status> spawnGameRoomIsolate(String roomUuid) async {
    try {
      _logger.finest('Spawning isolate for room $roomUuid');
      if (_gameRoomSendPorts.containsKey(roomUuid)) {
        _logger.severe('Tried to create a sendPort for an existing room uuid: $roomUuid', null, StackTrace.current);
        throw Exception('Cannot create sendPort, room already has one');
      }
      final receivePort = ReceivePort();
      final sp = receivePort.sendPort;
      await Isolate.spawn(LiveGameRoom.spawn, LiveGameRoomData(roomUuid: roomUuid, wsSendPort: sp));
      // used to get sendport
      final completer = Completer<SendPort>();

      // ignore: cancel_subscriptions
      final sub = receivePort.listen((message) => _gameRoomPortListener(message, completer));

      _gameRoomPortSubs[roomUuid] = sub;
      _gameRoomSendPorts[roomUuid] = await completer.future;
      _logger.info('Spawned new game room $roomUuid, listening to messages from room.');

      return Status.success;
    } catch (e) {
      _logger.severe('Failed to spawn game room isolate', e, StackTrace.current);
      _gameRoomSendPorts.remove(roomUuid);
      final sub = _gameRoomPortSubs.remove(roomUuid);
      await sub?.cancel();
    }
    return Status.failure;
  }

  void _gameRoomPortListener(dynamic message, Completer<SendPort> completer) {
    // first message from new isolate will be its SendPort
    if (!completer.isCompleted) {
      completer.complete(message as SendPort);
      return;
    }
    if (message is GameRoomMessage) {
      switch (message) {
        case PlayerVoteMessage():
          // TODO: Handle this case.
          throw UnimplementedError();
        case PingMessage():
          if (message.dest != PingDestination.client) {
            _logger.warning('Got room ping meant for server');
            return;
          }
          broadcastToRoom(
            message.roomUuid,
            message,
          );
      }
    } else {
      _logger.info('Unknown message: $message');
    }
  }

  void _gameRoomMessageListener(dynamic message) {}
}

enum Status { success, failure }