#if defined(WITH_ONLINE_MULTIPLAYER)
#	define ENET_IMPLEMENTATION
#endif
#include "NetworkManagerBase.h"

#if defined(WITH_MULTIPLAYER)

#include "INetworkHandler.h"
#include "PacketTypes.h"
#include "../../nCine/Base/Algorithms.h"
#include "../../nCine/Threading/Thread.h"

#include <atomic>
#include <mutex>

#include <Environment.h>
#include <Containers/GrowableArray.h>
#include <Containers/String.h>
#include <Containers/StringConcatenable.h>
#include <Containers/StringStl.h>
#include <Containers/StringStlView.h>
#include <IO/FileSystem.h>

#if defined(DEATH_TARGET_ANDROID)
#	include <enet/ifaddrs-android.h>
#elif defined(DEATH_TARGET_SWITCH)
#	include <net/if.h>
#elif defined(DEATH_TARGET_WINDOWS)
#	include <iphlpapi.h>
#elif !defined(DEATH_TARGET_EMSCRIPTEN)
#	include <ifaddrs.h>
#endif

#if defined(WITH_WEBSOCKET)
#	if defined(DEATH_TARGET_EMSCRIPTEN)
#		define EM_WS_ERR_SECURITY -1001
#		define EM_WS_ERR_DOM -1002
#		define EM_WS_ERR_MALFORMED_URL -1003
#		define EM_WS_ERR_UNKNOWN -1004

// emscripten_websocket_new doesn't correctly catch exceptions in Emscripten 5.0.7, so this wrapper
// handles it and returns error codes instead of throwing, to avoid crashing the entire application
EM_JS(EMSCRIPTEN_WEBSOCKET_T, safe_websocket_new, (const EmscriptenWebSocketCreateAttributes* attrs), {
	try {
		return _emscripten_websocket_new(attrs);
	} catch (e) {
		if (e instanceof DOMException) {
			// SecurityError: ws:// from https://
			if (e.name == 'SecurityError') return -1001;
			return -1002;
		} else if (e instanceof SyntaxError) {
			// Malformed URL
			return -1003;
		} else {
			return -1004;
		}
	}
});
#	else
#		include <ixwebsocket/IXNetSystem.h>
#	endif
#endif

using namespace Death;
using namespace Death::Containers::Literals;

namespace Jazz2::Multiplayer
{
	static std::atomic_int32_t _initializeCount{0};

#if defined(WITH_WEBSOCKET)
	static constexpr std::uint32_t WsPingIntervalMs = 10000;

	/** @brief Returns the current monotonic time in milliseconds */
	static DEATH_ALWAYS_INLINE std::uint64_t GetCurrentTimeMs()
	{
		return Environment::QueryUnbiasedInterruptTimeAsMs();
	}

	/** @brief Formats client data as a WebSocket sub-protocol string: "death-v0x<hex>" */
	static String MakeClientSubProtocol(std::uint32_t clientData)
	{
		return format("death-v0x{:x}", clientData);
	}

	/** @brief Parses client data from a sub-protocol string of the form "death-v0x<hex>" */
	static std::uint32_t ParseProtocolClientData(StringView protocol)
	{
		const StringView prefix = "death-v0x"_s;
		if (!protocol.hasPrefix(prefix)) {
			return 0;
		}

		StringView hexPart = protocol.exceptPrefix(prefix.size());
		if (hexPart.empty()) {
			return 0;
		}

		std::uint32_t result = 0;
		for (char c : hexPart) {
			int val = -1;
			if (c >= '0' && c <= '9') val = c - '0';
			else if (c >= 'a' && c <= 'f') val = c - 'a' + 10;
			else if (c >= 'A' && c <= 'F') val = c - 'A' + 10;
			else return 0;
			result = result * 16 + static_cast<std::uint32_t>(val);
		}
		return result;
	}

	/** @brief Extracts only the host[:port] from a WebSocket URL, stripping protocol prefix, path, and query string */
	static StringView ExtractHostFromWsUrl(StringView url)
	{
		if (url.hasPrefix("wss://"_s)) {
			url = url.exceptPrefix(6);
		} else if (url.hasPrefix("ws://"_s)) {
			url = url.exceptPrefix(5);
		}
		auto end = url.findAnyOr("/?"_s, url.end()).begin();
		return url.prefix(end);
	}

	/** @brief Converts a disconnect Reason to the corresponding WebSocket close code */
	static std::uint16_t ReasonToWsCloseCode(Reason reason)
	{
		switch (reason) {
			case Reason::Disconnected:
				return 1000;	// Normal Closure
			case Reason::Kicked:
				return 1008;	// Policy Violation
			default:
				return std::uint16_t(4000) + std::uint16_t(reason);
		}
	}

	/** @brief Converts a WebSocket close code back to a disconnect Reason */
	static Reason WsCloseCodeToReason(std::uint16_t code, bool wasConnected)
	{
		switch (code) {
			case 1000:	// Normal Closure
			case 1001:	// Going Away
				return Reason::Disconnected;
			case 1002:	// Protocol Error
			case 1003:	// Unsupported Data
			case 1007:	// Invalid Payload Data
			case 1010:	// Mandatory Extension
			case 1015:	// TLS Handshake Failed
				return Reason::ProtocolViolation;
			case 1008:	// Policy Violation
				return Reason::Kicked;
			case 1012:	// Service Restart
			case 1013:	// Try Again Later
				return Reason::ServerNotReady;
			default:
				if (code >= 4000 && code < std::uint16_t(4000 + std::uint32_t(Reason::Idle) + 1)) {
					return Reason(code - 4000);
				}
				return (wasConnected ? Reason::ConnectionLost : Reason::ConnectionTimedOut);
		}
	}
#endif

	NetworkManagerBase::NetworkManagerBase()
		:
#if !defined(DEATH_TARGET_EMSCRIPTEN)
		_host(nullptr),
#endif
		_state(NetworkState::None), _handler(nullptr)
	{
		InitializeBackend();
	}

	NetworkManagerBase::~NetworkManagerBase()
	{
		Dispose();
		ReleaseBackend();
	}

	void NetworkManagerBase::CreateClient(INetworkHandler* handler, StringView endpoints, std::uint16_t defaultPort, std::uint32_t clientData)
	{
		if (_handler != nullptr) {
			LOGE("Multiplayer client already created");
			return;
		}

		_state = NetworkState::Connecting;
		_clientData = clientData;
		_handler = handler;

#if defined(WITH_ONLINE_MULTIPLAYER)
#	if defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_WEBSOCKET)
	StringView firstEndpoint = endpoints.prefix(endpoints.findOr('|', endpoints.end()).begin());
	String proto = MakeClientSubProtocol(clientData);

	EmscriptenWebSocketCreateAttributes attrs;
	emscripten_websocket_init_create_attributes(&attrs);
	attrs.url = firstEndpoint.data();
	attrs.protocols = proto.data();
	attrs.createOnMainThread = EM_TRUE;

	// emscripten_websocket_new doesn't correctly catches exceptions in Emscripten 5.0.7
	//_emWsSocket = emscripten_websocket_new(&attrs);
	_emWsSocket = safe_websocket_new(&attrs);
		if (_emWsSocket <= 0) {
			Reason reason;
			switch (_emWsSocket) {
				case EM_WS_ERR_SECURITY:
					LOGE("WebSocket connection to \"{}\" failed due to security error (e.g., \"ws://\" from \"https://\")", firstEndpoint);
					reason = Reason::SecurityPolicyViolation;
					break;
				case EM_WS_ERR_DOM:
					LOGE("WebSocket connection to \"{}\" failed due to DOM error (e.g., browser blocked the connection)", firstEndpoint);
					reason = Reason::SecurityPolicyViolation;
					break;
				case EM_WS_ERR_MALFORMED_URL:
					LOGE("WebSocket connection to \"{}\" failed due to malformed URL", firstEndpoint);
					reason = Reason::InvalidParameter;
					break;
				default:
					LOGE("WebSocket connection to \"{}\" failed due to unknown error", firstEndpoint);
					reason = Reason::Unknown;
					break;
			}

			OnPeerDisconnected({}, reason);
			_state = NetworkState::None;
			_handler = nullptr;
			return;
		}

		_emWsUrl = firstEndpoint;
		LOGI("Connecting to \"{}\"", _emWsUrl);

		emscripten_websocket_set_onopen_callback(_emWsSocket, this, OnEmWsOpen);
		emscripten_websocket_set_onmessage_callback(_emWsSocket, this, OnEmWsMessage);
		emscripten_websocket_set_onerror_callback(_emWsSocket, this, OnEmWsError);
		emscripten_websocket_set_onclose_callback(_emWsSocket, this, OnEmWsClose);
#	else
#		if defined(WITH_WEBSOCKET)
		// If the first endpoint looks like a WebSocket URL, use IXWebSocket client
		StringView firstEndpoint = endpoints.prefix(endpoints.findOr('|', endpoints.end()).begin());
		if (firstEndpoint.hasPrefix("ws://"_s) || firstEndpoint.hasPrefix("wss://"_s)) {
			LOGI("Connecting to \"{}\" via WebSocket transport", firstEndpoint);

			_wsClient = std::make_unique<ix::WebSocket>();
			_wsClient->setUrl(firstEndpoint);
			_wsClient->addSubProtocol(MakeClientSubProtocol(clientData));
			_wsClient->setExtraHeaders({
				{"User-Agent", "Jazz2 Resurrection"}
			});
			_wsClient->disableAutomaticReconnection();
			_wsClient->setOnMessageCallback([this](const ix::WebSocketMessagePtr& msg) {
				ix::WebSocket* ws = _wsClient.get();
				if (msg->type == ix::WebSocketMessageType::Open) {
					std::unique_lock<Spinlock> lock(_wsLock);
					_wsPeers.emplace(ws, WsPeerInfo{});
					_wsPendingEvents.push_back({WsQueuedEvent::Type::Open, ws, {}, _clientData, 0});
				} else if (msg->type == ix::WebSocketMessageType::Close) {
					std::unique_lock<Spinlock> lock(_wsLock);
					_wsPeers.erase(ws);
					_wsPendingEvents.push_back({WsQueuedEvent::Type::Close, ws, {}, 0, msg->closeInfo.code});
				} else if (msg->type == ix::WebSocketMessageType::Message && msg->binary) {
					if (!msg->str.empty()) {
						std::unique_lock<Spinlock> lock(_wsLock);
						_wsPendingEvents.push_back({WsQueuedEvent::Type::Message, ws, msg->str, 0, 0});
					}
				} else if (msg->type == ix::WebSocketMessageType::Error) {
					LOGE("WebSocket transport error: {}", StringView{msg->errorInfo.reason});
					_wsPendingEvents.push_back({WsQueuedEvent::Type::Close, ws, {}, 0, 0});
				}
			});
			_wsClient->start();
			_thread = Thread(NetworkManagerBase::OnClientWsThread, this);
			return;
		}
#		endif

		_desiredEndpoints.clear();

#		if defined(DEATH_TARGET_ANDROID)
		std::int32_t ifidx = 0;
		struct ifaddrs* ifaddr;
		struct ifaddrs* ifa;
		if (getifaddrs(&ifaddr) == 0) {
			for (ifa = ifaddr; ifa != nullptr; ifa = ifa->ifa_next) {
				if (ifa->ifa_addr != nullptr && (ifa->ifa_addr->sa_family == AF_INET || ifa->ifa_addr->sa_family == AF_INET6) &&
					(ifa->ifa_flags & IFF_UP)) {
					ifidx = if_nametoindex(ifa->ifa_name);
					if (ifidx > 0) {
						LOGI("Using {} interface \"{}\" ({})", ifa->ifa_addr->sa_family == AF_INET6
							? "IPv6" : "IPv4", ifa->ifa_name, ifidx);
						break;
					}
				}
			}
			freeifaddrs(ifaddr);
		}
		if (ifidx == 0) {
			LOGI("No suitable network interface found");
			ifidx = if_nametoindex("wlan0");
		}
#		else
		std::int32_t ifidx = 0;
#		endif

		while (endpoints) {
			auto p = endpoints.partition('|');
			if (p[0]) {
				StringView address; std::uint16_t port;
				if (TrySplitAddressAndPort(p[0], address, port)) {
					ENetAddress addr = {};
					String nullTerminatedAddress = String::nullTerminatedView(address);
					std::int32_t r = enet_address_set_host(&addr, nullTerminatedAddress.data());
					//std::int32_t r = enet_address_set_host_ip(&addr, nullTerminatedAddress.data());
					if (r == 0) {
#		if ENET_IPV6
						if (addr.sin6_scope_id == 0) {
							addr.sin6_scope_id = (std::uint16_t)ifidx;
						}
#		endif
						addr.port = (port != 0 ? port : defaultPort);
						_desiredEndpoints.push_back(std::move(addr));
					} else {
#		if defined(DEATH_TARGET_WINDOWS)
						std::int32_t error = ::WSAGetLastError();
#		else
						std::int32_t error = errno;
#		endif
						LOGW("Failed to parse specified address \"{}\" with error {}", nullTerminatedAddress, error);
					}
				} else {
					LOGW("Failed to parse specified endpoint \"{}\"", p[0]);
				}
			}

			endpoints = p[2];
		}

		_thread = Thread(NetworkManagerBase::OnClientThread, this);
#	endif
#else
		// Online multiplayer (transport) is not compiled into this build
		LOGE("Online multiplayer is not available in this build");
		_state = NetworkState::None;
		_handler = nullptr;
#endif
	}

	bool NetworkManagerBase::CreateServer(INetworkHandler* handler, std::uint16_t port)
	{
#if !defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_ONLINE_MULTIPLAYER)
		if (_handler != nullptr) {
			return false;
		}

		ENetAddress addr = {};
		addr.host = ENET_HOST_ANY;
		addr.port = port;

		_host = enet_host_create(&addr, MaxPeerCount, (std::size_t)NetworkChannel::Count, 0, 0);
		DEATH_ASSERT(_host != nullptr, "Failed to create a server", false);

		_handler = handler;
		_state = NetworkState::Listening;
		_thread = Thread(NetworkManagerBase::OnServerThread, this);
		return true;
#else
		// Creating a server requires the online transport (not available on Emscripten or in local-only builds)
		return false;
#endif
	}

	void NetworkManagerBase::CreateLocalSession(INetworkHandler* handler)
	{
		// No socket is bound and no background thread is started; the manager just reports a local session state
		_state = NetworkState::Local;
		_handler = handler;
	}

	void NetworkManagerBase::Dispose()
	{
#if defined(WITH_ONLINE_MULTIPLAYER)
#	if defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_WEBSOCKET)
		if (_emWsSocket <= 0) {
			return;
		}
		if (_emWsPingTimerId != 0) {
			emscripten_clear_timeout(_emWsPingTimerId);
			_emWsPingTimerId = 0;
		}
		EMSCRIPTEN_WEBSOCKET_T socket = _emWsSocket;
		bool wasConnected = (_state == NetworkState::Connected);
		_emWsSocket = 0;
		_emWsRtt = 0;
		_state = NetworkState::None;
		// Delete before calling OnPeerDisconnected to prevent any callbacks firing
		emscripten_websocket_close(socket, 1000, "Client disconnecting");
		emscripten_websocket_delete(socket);
		if (wasConnected) {
			OnPeerDisconnected(Peer::FromWebSocket(1), Reason::Disconnected);
		}
#	else
		if (_host == nullptr
#		if defined(WITH_WEBSOCKET)
			&& _wsClient == nullptr
#		endif
		) {
			return;
		}

		_state = NetworkState::None;
		_thread.Join();

		_host = nullptr;
#	endif
#endif

		_handler = nullptr;
	}

	NetworkState NetworkManagerBase::GetState() const
	{
		return _state;
	}

	std::uint32_t NetworkManagerBase::GetRoundTripTimeMs() const
	{
#if defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_WEBSOCKET)
		return _emWsRtt;
#else
		// Guard against the network thread erasing _connectedPeers (disconnect) while the UI polls the RTT
		std::unique_lock<Spinlock> lock(_lock);
		if (_state == NetworkState::Connected && !_connectedPeers.empty()) {
#	if defined(WITH_WEBSOCKET)
			if DEATH_UNLIKELY(_connectedPeers[0].IsWebSocket()) {
				return _wsRtt.load(std::memory_order_relaxed);
			}
#	endif
			return _connectedPeers[0]._enet->roundTripTime;
		}
		return 0;
#endif
	}

	std::uint32_t NetworkManagerBase::GetRoundTripTimeMs(const Peer& peer) const
	{
#if defined(DEATH_TARGET_EMSCRIPTEN)
		return 0;
#else
#	if defined(WITH_WEBSOCKET)
		if DEATH_UNLIKELY(peer.IsWebSocket()) {
			std::unique_lock<Spinlock> lock(_wsLock);
			auto it = _wsPeers.find(peer._ws);
			if DEATH_LIKELY(it != _wsPeers.end()) {
				return it->second.rtt;
			}
			return 0;
		}
#	endif
		return (peer._enet != nullptr ? peer._enet->roundTripTime : 0);
#endif
	}

	Array<String> NetworkManagerBase::GetServerEndpoints() const
	{
#if defined(DEATH_TARGET_EMSCRIPTEN)
		// Creating a server is not supported on Emscripten
		return {};
#else
		Array<String> result;

		// _host and _connectedPeers are torn down by the network thread under _lock
		std::unique_lock lock(_lock);

		if (_state == NetworkState::Listening && _host != nullptr) {
#	if defined(DEATH_TARGET_SWITCH)
			struct ifconf ifc;
			struct ifreq ifr[8];
			ifc.ifc_len = sizeof(ifr);
			ifc.ifc_req = ifr;

			if (ioctl(_host->socket, SIOCGIFCONF, &ifc) >= 0) {
				std::int32_t count = ifc.ifc_len / sizeof(struct ifreq);
				LOGI("Found {} interfaces:", count);
				for (std::int32_t i = 0; i < count; i++) {
					if (ifr[i].ifr_addr.sa_family == AF_INET) { // IPv4
						auto* addrPtr = &((struct sockaddr_in*)&ifr[i].ifr_addr)->sin_addr;
						String addressString = AddressToString(*addrPtr, _host->address.port);
						LOGI(" -\t{}: {}", ifr[i].ifr_name, addressString);
						if (!addressString.empty() && !addressString.hasPrefix("127.0.0.1:"_s)) {
							arrayAppend(result, std::move(addressString));
						}
					}
#		if ENET_IPV6
					else if (ifr[i].ifr_addr.sa_family == AF_INET6) { // IPv6
						auto* addrPtr = &((struct sockaddr_in6*)&ifr[i].ifr_addr)->sin6_addr;
						//auto scopeId = ((struct sockaddr_in6*)&ifr[i].ifr_addr)->sin6_scope_id;
						String addressString = AddressToString(*addrPtr, /*scopeId*/0, _host->address.port);
						LOGI(" -\t{}: {}", ifr[i].ifr_name, addressString);
						if (!addressString.empty() && !addressString.hasPrefix("[::1]:"_s)) {
							arrayAppend(result, std::move(addressString));
						}
					}
#		endif
				}
			} else {
				LOGW("Failed to get server endpoints");
			}
#	elif defined(DEATH_TARGET_WINDOWS)
			ULONG bufferSize = 0;
			::GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, NULL, NULL, &bufferSize);
			std::unique_ptr<std::uint8_t[]> buffer = std::make_unique<std::uint8_t[]>(bufferSize);
			auto* adapterAddresses = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buffer.get());

			if (::GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, NULL, adapterAddresses, &bufferSize) == NO_ERROR) {
				for (auto* adapter = adapterAddresses; adapter != NULL; adapter = adapter->Next) {
					for (auto* address = adapter->FirstUnicastAddress; address != NULL; address = address->Next) {
						String addressString;
						if (address->Address.lpSockaddr->sa_family == AF_INET) { // IPv4
							auto* addrPtr = &((struct sockaddr_in*)address->Address.lpSockaddr)->sin_addr;
							String addressString = AddressToString(*addrPtr, _host->address.port);
							if (!addressString.empty() && !addressString.hasPrefix("127.0.0.1:"_s)) {
								arrayAppend(result, std::move(addressString));
							}
						}
#		if ENET_IPV6
						else if (address->Address.lpSockaddr->sa_family == AF_INET6) { // IPv6
							auto* addrPtr = &((struct sockaddr_in6*)address->Address.lpSockaddr)->sin6_addr;
							//auto scopeId = ((struct sockaddr_in6*)address->Address.lpSockaddr)->sin6_scope_id;
							String addressString = AddressToString(*addrPtr, /*scopeId*/0, _host->address.port);
							if (!addressString.empty() && !addressString.hasPrefix("[::1]:"_s)) {
								arrayAppend(result, std::move(addressString));
							}
						}
#		endif
						else {
							// Unsupported address family
						}
					}
				}
			} else {
				LOGW("Failed to get server endpoints");
			}
#	else
			struct ifaddrs* ifAddrStruct = nullptr;
			if (getifaddrs(&ifAddrStruct) == 0) {
				for (struct ifaddrs* ifa = ifAddrStruct; ifa != nullptr; ifa = ifa->ifa_next) {
					if (ifa->ifa_addr == nullptr) {
						continue;
					}

					if (ifa->ifa_addr->sa_family == AF_INET) { // IPv4
						auto* addrPtr = &((struct sockaddr_in*)ifa->ifa_addr)->sin_addr;
						String addressString = AddressToString(*addrPtr, _host->address.port);
						if (!addressString.empty() && !addressString.hasPrefix("127.0.0.1:"_s)) {
							arrayAppend(result, std::move(addressString));
						}
					}
#		if ENET_IPV6
					else if (ifa->ifa_addr->sa_family == AF_INET6) { // IPv6
						auto* addrPtr = &((struct sockaddr_in6*)ifa->ifa_addr)->sin6_addr;
						//auto scopeId = ((struct sockaddr_in6*)ifa->ifa_addr)->sin6_scope_id;
						String addressString = AddressToString(*addrPtr, /*scopeId*/0, _host->address.port);
						if (!addressString.empty() && !addressString.hasPrefix("[::1]:"_s)) {
							arrayAppend(result, std::move(addressString));
						}
					}
#		endif
				}
				freeifaddrs(ifAddrStruct);
			} else {
				LOGW("Failed to get server endpoints");
			}
#	endif
		} else {
			if (!_connectedPeers.empty()
#	if defined(WITH_WEBSOCKET)
				&& !_connectedPeers[0].IsWebSocket()
#	endif
			) {
				String addressString = AddressToString(_connectedPeers[0]._enet->address, true);
				if (!addressString.empty() && !addressString.hasPrefix("[::1]:"_s)) {
					arrayAppend(result, std::move(addressString));
				}
			}
		}

		return result;
#endif
	}

	std::uint16_t NetworkManagerBase::GetServerPort() const
	{
#if defined(DEATH_TARGET_EMSCRIPTEN)
		// Creating a server is not supported on Emscripten
		return 0;
#else
		// _host and _connectedPeers are torn down by the network thread under _lock
		std::unique_lock lock(_lock);
		if (_state == NetworkState::Listening && _host != nullptr) {
			return _host->address.port;
		} else if (!_connectedPeers.empty()
#	if defined(WITH_WEBSOCKET)
			&& !_connectedPeers[0].IsWebSocket()
#	endif
		) {
			return _connectedPeers[0]._enet->address.port;
		} else {
			return 0;
		}
#endif
	}

	void NetworkManagerBase::SendTo(const Peer& peer, NetworkChannel channel, std::uint8_t packetType, ArrayView<const std::uint8_t> data)
	{
		if DEATH_UNLIKELY(_state == NetworkState::Local) {
			// Local session has no peers to send to
			return;
		}
#if defined(WITH_ONLINE_MULTIPLAYER)
#	if defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_WEBSOCKET)
		if DEATH_LIKELY(_emWsSocket > 0) {
			SmallVector<std::uint8_t, 128> buf(1 + data.size());
			buf[0] = packetType;
			if (!data.empty()) {
				std::memcpy(buf.data() + 1, data.data(), data.size());
			}
			emscripten_websocket_send_binary(_emWsSocket, buf.data(), buf.size());
		}
#	else
#		if defined(WITH_WEBSOCKET)
		if DEATH_UNLIKELY(peer.IsWebSocket()) {
			SendToWsPeer(peer._ws, packetType, data);
			return;
		}
#		endif

		enet_uint32 flags;
		if (channel == NetworkChannel::Main) {
			flags = ENET_PACKET_FLAG_RELIABLE;
		} else {
			flags = ENET_PACKET_FLAG_UNSEQUENCED;
		}

		ENetPacket* packet = enet_packet_create(packetType, data.data(), data.size(), flags);

		bool success = false;
		{
			// The target is resolved under the lock too, so the network thread can't tear down
			// _connectedPeers/_host between the check and the send
			std::unique_lock lock(_lock);
			ENetPeer* target;
			if (peer == nullptr) {
				target = (_state == NetworkState::Connected && !_connectedPeers.empty()
#		if defined(WITH_WEBSOCKET)
					&& !_connectedPeers[0].IsWebSocket()
#		endif
					? _connectedPeers[0]._enet : nullptr);
			} else {
				target = peer._enet;
			}
			if DEATH_LIKELY(target != nullptr) {
				success = enet_peer_send(target, std::uint8_t(channel), packet) >= 0;
			}
		}

		if DEATH_UNLIKELY(!success) {
			enet_packet_destroy(packet);
		}
#	endif
#endif
	}

	void NetworkManagerBase::SendTo(Function<bool(const Peer&)>&& predicate, NetworkChannel channel, std::uint8_t packetType, ArrayView<const std::uint8_t> data)
	{
		if DEATH_UNLIKELY(_state == NetworkState::Local) {
			// Local session has no peers to send to
			return;
		}
#if defined(WITH_ONLINE_MULTIPLAYER)
#	if defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_WEBSOCKET)
		if DEATH_LIKELY(_emWsSocket > 0) {
			Peer serverPeer = Peer::FromWebSocket(1);
			if (predicate(serverPeer)) {
				SmallVector<std::uint8_t, 128> buffer(1 + data.size());
				buffer[0] = packetType;
				if (!data.empty()) {
					std::memcpy(buffer.data() + 1, data.data(), data.size());
				}
				emscripten_websocket_send_binary(_emWsSocket, buffer.data(), buffer.size());
			}
		}
#	else
		enet_uint32 flags;
		if (channel == NetworkChannel::Main) {
			flags = ENET_PACKET_FLAG_RELIABLE;
		} else {
			flags = ENET_PACKET_FLAG_UNSEQUENCED;
		}

		// The predicate is evaluated without holding _lock - predicates commonly call GetPeerDescriptor(),
		// which takes the derived class lock, and holding _lock across that would invert the lock order
		// against callers that hold the derived lock while sending/kicking (deadlock with spinlocks)
		SmallVector<Peer, 16> targets;
		{
			std::unique_lock lock(_lock);
			targets.assign(_connectedPeers.begin(), _connectedPeers.end());
		}

		SmallVector<Peer, 16> enetTargets;
#		if defined(WITH_WEBSOCKET)
		SmallVector<ix::WebSocket*, 16> wsTargets;
#		endif
		for (const Peer& p : targets) {
			if (predicate(p)) {
#		if defined(WITH_WEBSOCKET)
				if DEATH_UNLIKELY(p.IsWebSocket()) {
					wsTargets.push_back(p._ws);
				} else
#		endif
				{
					enetTargets.push_back(p);
				}
			}
		}

		ENetPacket* enetPacket = nullptr;
		bool enetPacketSent = false;
		if (!enetTargets.empty()) {
			std::unique_lock lock(_lock);
			for (const Peer& p : enetTargets) {
				// Re-check the peer is still connected - it may have been torn down while the predicate ran unlocked
				bool stillConnected = false;
				for (const Peer& c : _connectedPeers) {
					if (c == p) {
						stillConnected = true;
						break;
					}
				}
				if DEATH_UNLIKELY(!stillConnected) {
					continue;
				}
				if DEATH_UNLIKELY(enetPacket == nullptr) {
					enetPacket = enet_packet_create(packetType, data.data(), data.size(), flags);
				}
				if DEATH_LIKELY(enet_peer_send(p._enet, std::uint8_t(channel), enetPacket) >= 0) {
					enetPacketSent = true;
				}
			}
		}

		if (enetPacket != nullptr && !enetPacketSent) {
			enet_packet_destroy(enetPacket);
		}

#		if defined(WITH_WEBSOCKET)
		if (!wsTargets.empty()) {
			std::string wsPacket(1 + data.size(), '\0');
			wsPacket[0] = (char)packetType;
			if (!data.empty()) {
				std::memcpy(&wsPacket[1], data.data(), data.size());
			}
			for (ix::WebSocket* ws : wsTargets) {
				// Send under the lock while the peer is confirmed present, so the socket can't be
				// destroyed between the lookup and the send
				std::unique_lock<Spinlock> wslock(_wsLock);
				if DEATH_LIKELY(_wsPeers.find(ws) != _wsPeers.end()) {
					ws->sendBinary(wsPacket);
				}
			}
		}
#		endif
#	endif
#endif
	}

	void NetworkManagerBase::SendTo(AllPeersT, NetworkChannel channel, std::uint8_t packetType, ArrayView<const std::uint8_t> data)
	{
		if DEATH_UNLIKELY(_state == NetworkState::Local) {
			// Local session has no peers to send to
			return;
		}
#if defined(WITH_ONLINE_MULTIPLAYER)
#	if defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_WEBSOCKET)
		if DEATH_LIKELY(_emWsSocket > 0) {
			SmallVector<std::uint8_t, 128> buffer(1 + data.size());
			buffer[0] = packetType;
			if (!data.empty()) {
				std::memcpy(buffer.data() + 1, data.data(), data.size());
			}
			emscripten_websocket_send_binary(_emWsSocket, buffer.data(), buffer.size());
		}
#	else
		enet_uint32 flags;
		if (channel == NetworkChannel::Main) {
			flags = ENET_PACKET_FLAG_RELIABLE;
		} else {
			flags = ENET_PACKET_FLAG_UNSEQUENCED;
		}

		ENetPacket* enetPacket = nullptr;
		bool enetPacketSent = false;
#		if defined(WITH_WEBSOCKET)
		SmallVector<ix::WebSocket*, 16> wsTargets;
#		endif

		{
			std::unique_lock lock(_lock);
			for (const Peer& p : _connectedPeers) {
#		if defined(WITH_WEBSOCKET)
				if DEATH_UNLIKELY(p.IsWebSocket()) {
					wsTargets.push_back(p._ws);
				} else
#		endif
				{
					if DEATH_UNLIKELY(enetPacket == nullptr) {
						enetPacket = enet_packet_create(packetType, data.data(), data.size(), flags);
					}
					if DEATH_LIKELY(enet_peer_send(p._enet, std::uint8_t(channel), enetPacket) >= 0) {
						enetPacketSent = true;
					}
				}
			}
		}

		if (enetPacket != nullptr && !enetPacketSent) {
			enet_packet_destroy(enetPacket);
		}

#		if defined(WITH_WEBSOCKET)
		if (!wsTargets.empty()) {
			std::string wsPacket(1 + data.size(), '\0');
			wsPacket[0] = (char)packetType;
			if (!data.empty()) {
				std::memcpy(&wsPacket[1], data.data(), data.size());
			}
			for (ix::WebSocket* ws : wsTargets) {
				// Send under the lock while the peer is confirmed present, so the socket can't be
				// destroyed between the lookup and the send
				std::unique_lock<Spinlock> wslock(_wsLock);
				if DEATH_LIKELY(_wsPeers.find(ws) != _wsPeers.end()) {
					ws->sendBinary(wsPacket);
				}
			}
		}
#		endif
#	endif
#endif
	}

	void NetworkManagerBase::Kick(const Peer& peer, Reason reason)
	{
		if DEATH_UNLIKELY(_state == NetworkState::Local) {
			// Local session has no peers to kick
			return;
		}
#if defined(WITH_ONLINE_MULTIPLAYER)
#	if defined(WITH_WEBSOCKET) && !defined(DEATH_TARGET_EMSCRIPTEN)
		if DEATH_UNLIKELY(peer.IsWebSocket()) {
			// Close under the lock while the peer is confirmed present, so the socket can't be
			// destroyed between the lookup and the close
			std::unique_lock<Spinlock> lock(_wsLock);
			if DEATH_LIKELY(_wsPeers.find(peer._ws) != _wsPeers.end()) {
				peer._ws->close(ReasonToWsCloseCode(reason), ReasonToString(reason));
			}
			return;
		}
#	endif
#	if !defined(DEATH_TARGET_EMSCRIPTEN)
		if DEATH_LIKELY(peer != nullptr) {
			std::unique_lock lock(_lock);
			enet_peer_disconnect(peer._enet, std::uint32_t(reason));
		}
#	endif
#endif
	}


	String NetworkManagerBase::AddressToString(const struct in_addr& address, std::uint16_t port)
	{
#if defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_WEBSOCKET)
		// TODO: Implement this properly on Emscripten
		return {};
#else
		char addressString[64];

		if (inet_ntop(AF_INET, &address, addressString, sizeof(addressString) - 1) == NULL) {
			return {};
		}

		std::size_t addressLength = strnlen(addressString, sizeof(addressString));
		if (port != 0) {
			addressLength = addressLength + formatInto({ &addressString[addressLength], sizeof(addressString) - addressLength }, ":{}", port);
		}
		return String(addressString, addressLength);
#endif
	}

#if ENET_IPV6
	String NetworkManagerBase::AddressToString(const struct in6_addr& address, std::uint16_t scopeId, std::uint16_t port)
	{
#	if defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_WEBSOCKET)
		// TODO: Implement this properly on Emscripten
		return {};
#	else
		char addressString[92];
		std::size_t addressLength = 0;

		if (IN6_IS_ADDR_V4MAPPED(&address)) {
			struct in_addr buf;
#		if defined(WITH_ONLINE_MULTIPLAYER)
			enet_inaddr_map6to4(&address, &buf);
#		else
			// enet_inaddr_map6to4 just extracts the embedded IPv4 (the last 4 bytes of the v4-mapped IPv6 address)
			std::memcpy(&buf, reinterpret_cast<const std::uint8_t*>(&address) + 12, sizeof(buf));
#		endif

			if (inet_ntop(AF_INET, &buf, addressString, sizeof(addressString) - 1) == NULL) {
				return {};
			}

			addressLength = strnlen(addressString, sizeof(addressString));
		} else {
			if (inet_ntop(AF_INET6, (void*)&address, &addressString[1], sizeof(addressString) - 3) == NULL) {
				return {};
			}

			addressString[0] = '[';
			addressLength = strnlen(addressString, sizeof(addressString));

			if (scopeId != 0) {
				addressLength += formatInto({ &addressString[addressLength], sizeof(addressString) - addressLength }, "%{}", scopeId);
			}

			addressString[addressLength] = ']';
			addressLength++;
		}

		if (port != 0) {
			addressLength += formatInto({ &addressString[addressLength], sizeof(addressString) - addressLength }, ":{}", port);
		}
		return String(addressString, addressLength);
#	endif
	}
#endif

#if !defined(DEATH_TARGET_EMSCRIPTEN)
	String NetworkManagerBase::AddressToString(const ENetAddress& address, bool includePort)
	{
#	if ENET_IPV6
		return AddressToString(address.host, address.sin6_scope_id, includePort ? address.port : 0);
#	else
		return AddressToString(*(const struct in_addr*)&address.host, includePort ? address.port : 0);
#	endif
	}
#endif

	String NetworkManagerBase::AddressToString(const Peer& peer) const
	{
#if defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_WEBSOCKET)
		return ExtractHostFromWsUrl(_emWsUrl);
#else
#	if defined(WITH_WEBSOCKET)
		if DEATH_UNLIKELY(peer.IsWebSocket()) {
			std::unique_lock<Spinlock> lock(_wsLock);
			auto it = _wsPeers.find(peer._ws);
			if DEATH_LIKELY(it != _wsPeers.end()) {
				return it->second.address;
			}
			return {};
		}
#	endif

		if DEATH_LIKELY(peer._enet != nullptr) {
			return AddressToString(peer._enet->address);
		}
		return {};
#endif
	}

	bool NetworkManagerBase::IsAddressValid(StringView address)
	{
#if defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_WEBSOCKET)
		// TODO: Implement this properly on Emscripten
		return true;
#else
		auto nullTerminatedAddress = String::nullTerminatedView(address);
#	if ENET_IPV6
		struct sockaddr_in sa;
		struct sockaddr_in6 sa6;
		return (inet_pton(AF_INET6, nullTerminatedAddress.data(), &(sa6.sin6_addr)) == 1)
			|| (inet_pton(AF_INET, nullTerminatedAddress.data(), &(sa.sin_addr)) == 1);
#	else
		struct sockaddr_in sa;
		return (inet_pton(AF_INET, nullTerminatedAddress.data(), &(sa.sin_addr)) == 1);
#	endif
#endif
	}

	bool NetworkManagerBase::IsDomainValid(StringView domain)
	{
		if (domain.empty() || domain.size() > 253) {
			return false;
		}

		while (!domain.empty()) {
			StringView end = domain.findOr('.', domain.end());
			StringView part = domain.prefix(end.begin());
			if (part.size() < 1 || part.size() > 63) {
				return false;
			}

			for (char c : part) {
				if (!isalnum(c) && c != '-') {
					return false;
				}
			}

			// Part can't start or end with hyphen
			if (part[0] == '-' || part[part.size() - 1] == '-') {
				return false;
			}

			if (end.begin() == domain.end()) {
				break;
			}
			domain = domain.suffix(end.begin() + 1);
		}

		return true;
	}

	bool NetworkManagerBase::TrySplitAddressAndPort(StringView input, StringView& address, std::uint16_t& port)
	{
		if (auto portSep = input.findLast(':')) {
			auto portString = input.suffix(portSep.begin() + 1);
			if (portString.contains(']')) {
				// Probably only IPv6 address (or some garbage)
				address = input;
				port = 0;
				return true;
			} else {
				// Address (or hostname) and port
				address = input.prefix(portSep.begin()).trimmed();
				if (address.hasPrefix('[') && address.hasSuffix(']')) {
					address = address.slice(1, address.size() - 1);
				}
				if (address.empty()) {
					return false;
				}

				auto portString = input.suffix(portSep.begin() + 1);
				port = std::uint16_t(stou32(portString.data(), portString.size()));
				return true;
			}
		} else {
			// Address (or hostname) only
			address = input.trimmed();
			if (address.hasPrefix('[') && address.hasSuffix(']')) {
				address = address.slice(1, address.size() - 1);
			}
			if (address.empty()) {
				return false;
			}

			port = 0;
			return true;
		}
	}

	const char* NetworkManagerBase::ReasonToString(Reason reason)
	{
		switch (reason) {
			case Reason::Disconnected: return "Client disconnected by user"; break;
			case Reason::InvalidParameter: return "Invalid parameter specified"; break;
			case Reason::IncompatibleVersion: return "Incompatible client version"; break;
			case Reason::ProtocolViolation: return "Protocol violation"; break;
			case Reason::SecurityPolicyViolation: return "Security policy violation"; break;
			case Reason::AuthFailed: return "Authentication failed"; break;
			case Reason::InvalidPassword: return "Invalid password specified"; break;
			case Reason::InvalidPlayerName: return "Invalid player name specified"; break;
			case Reason::NotInWhitelist: return "Client is not in server whitelist"; break;
			case Reason::Requires3rdPartyAuthProvider: return "Server requires 3rd party authentication provider"; break;
			case Reason::ServerIsFull: return "Server is full or busy"; break;
			case Reason::ServerNotReady: return "Server is not ready yet"; break;
			case Reason::ServerStopped: return "Server is stopped for unknown reason"; break;
			case Reason::ServerStoppedForMaintenance: return "Server is stopped for maintenance"; break;
			case Reason::ServerStoppedForReconfiguration: return "Server is stopped for reconfiguration"; break;
			case Reason::ServerStoppedForUpdate: return "Server is stopped for update"; break;
			case Reason::ConnectionLost: return "Connection lost"; break;
			case Reason::ConnectionTimedOut: return "Connection timed out"; break;
			case Reason::Kicked: return "Kicked by server"; break;
			case Reason::Banned: return "Banned by server"; break;
			case Reason::CheatingDetected: return "Cheating detected"; break;
			case Reason::AssetStreamingNotAllowed: return "Downloading of assets is not allowed"; break;
			case Reason::Idle: return "Inactivity"; break;
			default: return "Unknown reason"; break;
		}
	}

	ConnectionResult NetworkManagerBase::OnPeerConnected(const Peer& peer, std::uint32_t clientData)
	{
		return _handler->OnPeerConnected(peer, clientData);
	}

	void NetworkManagerBase::OnPeerDisconnected(const Peer& peer, Reason reason)
	{
		_handler->OnPeerDisconnected(peer, reason);

#if !defined(DEATH_TARGET_EMSCRIPTEN)
		if (peer && _state == NetworkState::Listening) {
			std::unique_lock lock(_lock);
			for (std::size_t i = 0; i < _connectedPeers.size(); i++) {
				if (peer == _connectedPeers[i]) {
					_connectedPeers.eraseUnordered(i);
					break;
				}
			}
		}
#endif
	}

	void NetworkManagerBase::InitializeBackend()
	{
#if !defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_ONLINE_MULTIPLAYER)
		if (++_initializeCount == 1) {
			std::int32_t error = enet_initialize();
			DEATH_ASSERT(error == 0, ("enet_initialize() failed with error {}", error), );
		}
#endif
	}

	void NetworkManagerBase::ReleaseBackend()
	{
#if !defined(DEATH_TARGET_EMSCRIPTEN) && defined(WITH_ONLINE_MULTIPLAYER)
		if (--_initializeCount == 0) {
			enet_deinitialize();
		}
#endif
	}

#if defined(DEATH_TARGET_EMSCRIPTEN)

	EM_BOOL NetworkManagerBase::OnEmWsOpen(int eventType, const EmscriptenWebSocketOpenEvent* e, void* userData)
	{
		LOGI("WebSocket connection opened");

		NetworkManagerBase* _this = static_cast<NetworkManagerBase*>(userData);
		if (_this->_emWsSocket <= 0 || _this->_handler == nullptr) {
			return EM_TRUE;
		}
		_this->_state = NetworkState::Connected;
		Peer serverPeer = Peer::FromWebSocket(1);
		ConnectionResult result = _this->OnPeerConnected(serverPeer, _this->_clientData);
		if (!result.IsSuccessful()) {
			EMSCRIPTEN_WEBSOCKET_T socket = _this->_emWsSocket;
			_this->_emWsSocket = 0;
			_this->_state = NetworkState::None;
			emscripten_websocket_close(socket, ReasonToWsCloseCode(result.FailureReason), ReasonToString(result.FailureReason));
			emscripten_websocket_delete(socket);
			_this->_handler = nullptr;
		} else {
			// Schedule the first periodic Ping
			_this->_emWsPingTimerId = emscripten_set_timeout(OnEmWsPingTimer, (double)WsPingIntervalMs, _this);
		}
		return EM_TRUE;
	}

	EM_BOOL NetworkManagerBase::OnEmWsMessage(int eventType, const EmscriptenWebSocketMessageEvent* e, void* userData)
	{
		NetworkManagerBase* _this = static_cast<NetworkManagerBase*>(userData);
		if (_this->_emWsSocket <= 0 || _this->_handler == nullptr) {
			return EM_TRUE;
		}
		if (!e->isText && e->numBytes >= 1) {
			std::uint8_t pktType = (std::uint8_t)e->data[0];
			if DEATH_UNLIKELY(pktType == (std::uint8_t)ServerPacketType::Pong && e->numBytes >= 1 + 8) {
				// Intercept Pong: compute RTT from echoed timestamp
				std::uint64_t sentTime;
				std::memcpy(&sentTime, e->data + 1, 8);
				std::uint64_t rtt = GetCurrentTimeMs() - sentTime;
				_this->_emWsRtt = (std::uint32_t)rtt;
			} else {
				_this->_handler->OnPacketReceived(Peer::FromWebSocket(1), 0,
					pktType, arrayView((const std::uint8_t*)e->data + 1, e->numBytes - 1));
			}
		}
		return EM_TRUE;
	}

	EM_BOOL NetworkManagerBase::OnEmWsError(int eventType, const EmscriptenWebSocketErrorEvent* e, void* userData)
	{
		LOGE("WebSocket connection error");
		return EM_TRUE;
	}

	EM_BOOL NetworkManagerBase::OnEmWsClose(int eventType, const EmscriptenWebSocketCloseEvent* e, void* userData)
	{
		NetworkManagerBase* _this = static_cast<NetworkManagerBase*>(userData);
		if (_this->_emWsSocket <= 0 || _this->_handler == nullptr) {
			return EM_TRUE;
		}
		if (_this->_emWsPingTimerId != 0) {
			emscripten_clear_timeout(_this->_emWsPingTimerId);
			_this->_emWsPingTimerId = 0;
		}
		EMSCRIPTEN_WEBSOCKET_T socket = _this->_emWsSocket;
		bool wasConnected = (_this->_state == NetworkState::Connected);
		_this->_emWsSocket = 0;
		_this->_emWsRtt = 0;
		_this->_state = NetworkState::None;
		emscripten_websocket_delete(socket);
		_this->OnPeerDisconnected(
			wasConnected ? Peer::FromWebSocket(1) : Peer{},
			WsCloseCodeToReason(e->code, wasConnected));
		_this->_handler = nullptr;
		return EM_TRUE;
	}

	void NetworkManagerBase::OnEmWsPingTimer(void* userData)
	{
		NetworkManagerBase* _this = static_cast<NetworkManagerBase*>(userData);
		_this->_emWsPingTimerId = 0;
		if (_this->_emWsSocket <= 0 || _this->_state != NetworkState::Connected) {
			return;
		}

		std::uint64_t now = GetCurrentTimeMs();
		std::uint32_t lastRtt = _this->_emWsRtt;
		std::uint8_t pingBuf[1 + 8 + 4];
		pingBuf[0] = (std::uint8_t)ClientPacketType::Ping;
		std::memcpy(pingBuf + 1, &now, 8);
		std::memcpy(pingBuf + 9, &lastRtt, 4);
		emscripten_websocket_send_binary(_this->_emWsSocket, pingBuf, sizeof(pingBuf));

		// Reschedule
		_this->_emWsPingTimerId = emscripten_set_timeout(OnEmWsPingTimer, (double)WsPingIntervalMs, _this);
	}

#else
#	if defined(WITH_WEBSOCKET)

	bool NetworkManagerBase::StartWsServer(std::uint16_t wsPort, StringView certPath, StringView keyPath)
	{
		if (wsPort == 0) {
			return false;
		}

		DEATH_ASSERT(_wsServer == nullptr, "WebSocket transport already started", false);

		_wsServer = std::make_unique<ix::WebSocketServer>(wsPort, "0.0.0.0");
		_wsServer->setServerName("Jazz2 Resurrection");

#		if defined(WITH_WEBSOCKET_TLS)
		bool useTls = false;
		if (!certPath.empty() && !keyPath.empty()) {
			useTls = true;
			if (!fs::FileExists(certPath)) {
				LOGE("Cannot find TLS certificate file at \"{}\", falling back to non-TLS WebSocket transport", certPath);
				useTls = false;
			}
			if (!fs::FileExists(keyPath)) {
				LOGE("Cannot find TLS key file at \"{}\", falling back to non-TLS WebSocket transport", keyPath);
				useTls = false;
			}
		}

		if (useTls) {
			ix::SocketTLSOptions tlsOptions;
			tlsOptions.certFile = certPath;
			tlsOptions.keyFile = keyPath;
			tlsOptions.caFile = "NONE";
			tlsOptions.tls = true;
			tlsOptions.autoDetect = true;
			_wsServer->setTLSOptions(tlsOptions);
			LOGI("WebSocket transport starting with TLS on port {}", wsPort);
		} else
#		endif
		{
			LOGI("WebSocket transport starting on port {}", wsPort);
		}

		// Select the application-level sub-protocol carrying client data if present
		_wsServer->setProtocolSelectionCallback([](const std::vector<std::string>& requestedProtocols,
			std::string& selectedProtocol) -> bool {
			for (const auto& p : requestedProtocols) {
				if (p.rfind("death-v0x", 0) == 0) {
					selectedProtocol = p;
					return true;
				}
			}
			// No specific protocol requested, reject the connection
			return false;
		});

		_wsServer->setOnClientMessageCallback([this](std::shared_ptr<ix::ConnectionState> state,
			ix::WebSocket& ws, const ix::WebSocketMessagePtr& msg) {

			if (msg->type == ix::WebSocketMessageType::Open) {
				const std::string remoteIp = state->getRemoteIp();
				std::uint32_t peerClientData = ParseProtocolClientData(msg->openInfo.protocol);

				{
					std::unique_lock<Spinlock> lock(_wsLock);
					_wsPeers.emplace(&ws, WsPeerInfo{String(remoteIp.data(), remoteIp.size()), 0});
					_wsPendingEvents.push_back({WsQueuedEvent::Type::Open, &ws, {}, peerClientData, 0});
				}
				LOGD("WebSocket client connected [{}] from {}", Peer::FromWebSocket(&ws), StringView{remoteIp});

			} else if (msg->type == ix::WebSocketMessageType::Close) {
				{
					std::unique_lock<Spinlock> lock(_wsLock);
					_wsPeers.erase(&ws);
					_wsPendingEvents.push_back({WsQueuedEvent::Type::Close, &ws, {}, 0, std::uint16_t(msg->closeInfo.code)});
				}
				LOGD("WebSocket client disconnected [{}]", Peer::FromWebSocket(&ws));
			} else if (msg->type == ix::WebSocketMessageType::Message && msg->binary) {
				if (!msg->str.empty()) {
					std::unique_lock<Spinlock> lock(_wsLock);
					_wsPendingEvents.push_back({WsQueuedEvent::Type::Message, &ws, msg->str});
				}

			} else if (msg->type == ix::WebSocketMessageType::Error) {
				LOGE("WebSocket transport error: {}", StringView{msg->errorInfo.reason});
			}
		});

		auto res = _wsServer->listen();
		if (!res.first) {
			LOGE("WebSocket transport failed to listen on port {}: {}", wsPort, StringView{res.second});
			_wsServer = nullptr;
			return false;
		}

		_wsServer->start();
		return true;
	}

	void NetworkManagerBase::ProcessWsQueue(INetworkHandler* handler)
	{
		SmallVector<WsQueuedEvent, 0> pending;
		{
			std::unique_lock<Spinlock> lock(_wsLock);
			std::swap(pending, _wsPendingEvents);
		}

		for (auto& ev : pending) {
			switch (ev.type) {
				case WsQueuedEvent::Type::Open: {
					Peer wsPeer = Peer::FromWebSocket(ev.peer);
					ConnectionResult result = OnPeerConnected(wsPeer, ev.clientData);
					if (result.IsSuccessful()) {
						std::unique_lock lock(_lock);
						_connectedPeers.push_back(wsPeer);
					} else {
						// Reject the connection
						ev.peer->close(ReasonToWsCloseCode(result.FailureReason), ReasonToString(result.FailureReason));
					}
					break;
				}
				case WsQueuedEvent::Type::Close: {
					OnPeerDisconnected(Peer::FromWebSocket(ev.peer), WsCloseCodeToReason(ev.closeCode, true));
					break;
				}
				case WsQueuedEvent::Type::Message: {
					if DEATH_LIKELY(ev.data.size() >= 1) {
						std::uint8_t pktType = (std::uint8_t)ev.data[0];
						if DEATH_UNLIKELY(pktType == (std::uint8_t)ClientPacketType::Ping && ev.data.size() >= 1 + 8 + 4) {
							// Intercept Ping: read timestamp + reported RTT, update per-peer RTT, echo Pong
							std::uint64_t timestamp;
							std::uint32_t reportedRtt;
							std::memcpy(&timestamp, ev.data.data() + 1, 8);
							std::memcpy(&reportedRtt, ev.data.data() + 9, 4);
							{
								std::unique_lock<Spinlock> lock(_wsLock);
								auto it = _wsPeers.find(ev.peer);
								if DEATH_LIKELY(it != _wsPeers.end()) {
									it->second.rtt = reportedRtt;

									// Echo timestamp back as Pong. Done under the lock while the peer is confirmed
									// present, so the socket can't be destroyed between the lookup and the send.
									std::uint8_t pong[1 + 8];
									pong[0] = (std::uint8_t)ServerPacketType::Pong;
									std::memcpy(pong + 1, &timestamp, 8);
									ev.peer->sendBinary(std::string(reinterpret_cast<const char*>(pong), sizeof(pong)));
								}
							}
						} else {
							handler->OnPacketReceived(Peer::FromWebSocket(ev.peer), 0,
								pktType, arrayView((const std::uint8_t*)ev.data.data() + 1, ev.data.size() - 1));
						}
					}
					break;
				}
			}
		}
	}

	bool NetworkManagerBase::SendToWsPeer(ix::WebSocket* ws, std::uint8_t packetType, ArrayView<const std::uint8_t> data)
	{
		std::string packet(1 + data.size(), '\0');
		packet[0] = (char)packetType;
		if (!data.empty()) {
			std::memcpy(&packet[1], data.data(), data.size());
		}

		// Send under the lock while the peer is confirmed present, so the socket can't be
		// destroyed between the lookup and the send
		std::unique_lock<Spinlock> lock(_wsLock);
		if DEATH_UNLIKELY(_wsPeers.find(ws) == _wsPeers.end()) {
			return false;
		}
		ws->sendBinary(packet);
		return true;
	}

	void NetworkManagerBase::OnClientWsThread(void* param)
	{
		Thread::SetCurrentName("Multiplayer WebSocket client");

		NetworkManagerBase* _this = static_cast<NetworkManagerBase*>(param);
		INetworkHandler* handler = _this->_handler;

		bool wasConnected = false;
		ix::WebSocket* serverPeer = nullptr;
		Reason disconnectReason = Reason::ConnectionTimedOut;
		_this->_wsPingLastTime = 0;

		while (_this->_state != NetworkState::None) {
			Thread::Sleep(ProcessingIntervalMs);

			SmallVector<WsQueuedEvent, 0> pending;
			{
				std::unique_lock<Spinlock> lock(_this->_wsLock);
				std::swap(pending, _this->_wsPendingEvents);
			}

			for (auto& ev : pending) {
				switch (ev.type) {
					case WsQueuedEvent::Type::Open: {
						wasConnected = true;
						serverPeer = ev.peer;
						disconnectReason = Reason::Unknown;
						_this->_state = NetworkState::Connected;
						_this->_wsPingLastTime = GetCurrentTimeMs();
						Peer connectedPeer = Peer::FromWebSocket(ev.peer);
						{
							std::unique_lock lock(_this->_lock);
							_this->_connectedPeers.push_back(connectedPeer);
						}
						ConnectionResult result = _this->OnPeerConnected(connectedPeer, ev.clientData);
						if (!result.IsSuccessful()) {
							ev.peer->close(ReasonToWsCloseCode(result.FailureReason), ReasonToString(result.FailureReason));
						}
						break;
					}
					case WsQueuedEvent::Type::Close: {
						disconnectReason = WsCloseCodeToReason(ev.closeCode, wasConnected);
						_this->_state = NetworkState::None;
						break;
					}
					case WsQueuedEvent::Type::Message: {
						if (ev.data.size() >= 1) {
							std::uint8_t pktType = (std::uint8_t)ev.data[0];
							if DEATH_UNLIKELY(pktType == (std::uint8_t)ServerPacketType::Pong && ev.data.size() >= 1 + 8) {
								// Intercept Pong: compute RTT from echoed timestamp
								std::uint64_t sentTime;
								std::memcpy(&sentTime, ev.data.data() + 1, 8);
								std::uint64_t rtt = GetCurrentTimeMs() - sentTime;
								_this->_wsRtt.store((std::uint32_t)rtt, std::memory_order_relaxed);
							} else {
								handler->OnPacketReceived(Peer::FromWebSocket(ev.peer), 0,
									pktType, arrayView((const std::uint8_t*)ev.data.data() + 1, ev.data.size() - 1));
							}
						}
						break;
					}
				}
			}

			// Send periodic Ping to the server while connected
			if (wasConnected && serverPeer != nullptr && _this->_state == NetworkState::Connected) {
				std::uint64_t now = GetCurrentTimeMs();
				if DEATH_UNLIKELY(now - _this->_wsPingLastTime >= WsPingIntervalMs) {
					_this->_wsPingLastTime = now;
					std::uint32_t lastRtt = _this->_wsRtt.load(std::memory_order_relaxed);
					std::uint8_t pingBuf[1 + 8 + 4];
					pingBuf[0] = (std::uint8_t)ClientPacketType::Ping;
					std::memcpy(pingBuf + 1, &now, 8);
					std::memcpy(pingBuf + 9, &lastRtt, 4);
					serverPeer->sendBinary(std::string(reinterpret_cast<const char*>(pingBuf), sizeof(pingBuf)));
				}
			}
		}

		{
			std::unique_lock lock(_this->_lock);
			_this->_connectedPeers.clear();
		}

		if (wasConnected) {
			_this->OnPeerDisconnected(Peer::FromWebSocket(serverPeer), disconnectReason);
		} else {
			_this->OnPeerDisconnected({}, disconnectReason);
		}

		_this->_wsRtt.store(0, std::memory_order_relaxed);
		_this->_handler = nullptr;
		_this->_thread.Detach();

		LOGD("WebSocket client thread exited");
	}
#	endif

#	if defined(WITH_ONLINE_MULTIPLAYER)
	void NetworkManagerBase::OnClientThread(void* param)
	{
		Thread::SetCurrentName("Multiplayer client");

		NetworkManagerBase* _this = static_cast<NetworkManagerBase*>(param);
		INetworkHandler* handler = _this->_handler;

		ENetHost* host = nullptr;
		_this->_host = host;

		// Try to connect to each specified endpoint
		ENetEvent ev{};
		for (std::int32_t i = 0; i < std::int32_t(_this->_desiredEndpoints.size()) && _this->_state != NetworkState::None; i++) {
			ENetAddress& addr = _this->_desiredEndpoints[i];
			LOGI("Connecting to {} ({}/{})", _this->AddressToString(addr, true), i + 1, _this->_desiredEndpoints.size());
			
			if (host != nullptr) {
				enet_host_destroy(host);
			}

			host = enet_host_create(nullptr, 1, std::size_t(NetworkChannel::Count), 0, 0);
			_this->_host = host;
			if (host == nullptr) {
				LOGE("Failed to create multiplayer client");
				_this->OnPeerDisconnected({}, Reason::InvalidParameter);
				return;
			}

			ENetPeer* peer = enet_host_connect(host, &addr, std::size_t(NetworkChannel::Count), _this->_clientData);
			if (peer == nullptr) {
				continue;
			}

			std::int32_t n = 10;
			while (n > 0) {
				if (_this->_state == NetworkState::None) {
					n = 0;
					break;
				}

				LOGD("enet_host_service() is trying to connect: {} ms", enet_time_get());
				if (enet_host_service(host, &ev, 1000) > 0 && ev.type == ENET_EVENT_TYPE_CONNECT) {
					break;
				}

				n--;
			}

			if (n != 0) {
				{
					// The main thread may already be iterating _connectedPeers in SendTo() under _lock
					std::unique_lock lock(_this->_lock);
					_this->_connectedPeers.push_back(Peer(ev.peer));
				}
				break;
			}
		}

		Reason reason;
		if (_this->_connectedPeers.empty()) {
			LOGE("Failed to connect to the server");
			_this->_state = NetworkState::None;
			reason = Reason::ConnectionTimedOut;
		} else {
			_this->_state = NetworkState::Connected;
			_this->OnPeerConnected(ev.peer, ev.data);
			reason = Reason::Unknown;

			while DEATH_LIKELY(_this->_state != NetworkState::None) {
				std::int32_t result;
				{
					std::unique_lock lock(_this->_lock);
					result = enet_host_service(host, &ev, 0);
				}

				if DEATH_UNLIKELY(result <= 0) {
					if DEATH_UNLIKELY(result < 0) {
						LOGE("[MP] enet_host_service() returned {}", result);
						reason = Reason::ConnectionLost;
						break;
					}
					Thread::Sleep(ProcessingIntervalMs);
					continue;
				}

				switch (ev.type) {
					case ENET_EVENT_TYPE_RECEIVE: {
						// A valid packet carries at least the 1-byte packet-type prefix. Drop empty packets so the
						// data[0] read and exceptPrefix(1) slice below can't run out of bounds - a malicious or buggy
						// peer can legally send a zero-length packet, and exceptPrefix(1) on it would otherwise yield a
						// SIZE_MAX-length view in release builds (the bounds check is compiled out).
						if (ev.packet->dataLength >= 1) {
							auto data = arrayView(ev.packet->data, ev.packet->dataLength);
							handler->OnPacketReceived(ev.peer, ev.channelID, data[0], data.exceptPrefix(1));
						}
						enet_packet_destroy(ev.packet);
						break;
					}
					case ENET_EVENT_TYPE_DISCONNECT:
						_this->_state = NetworkState::None;
						reason = (Reason)ev.data;
						break;
					case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
						_this->_state = NetworkState::None;
						reason = Reason::ConnectionLost;
						break;
				}
			}
		}

		if DEATH_UNLIKELY(!_this->_connectedPeers.empty()) {
			// Called before taking _lock, because the handler acquires its own lock internally
			_this->OnPeerDisconnected(_this->_connectedPeers[0], reason);
		} else {
			_this->OnPeerDisconnected({}, reason);
		}

		{
			// Serialize the teardown with the main-thread send paths, which use _connectedPeers/_host under _lock
			std::unique_lock lock(_this->_lock);
			for (const Peer& p : _this->_connectedPeers) {
				enet_peer_disconnect_now(p._enet, (std::uint32_t)Reason::Disconnected);
			}
			_this->_connectedPeers.clear();

			enet_host_destroy(_this->_host);
			_this->_host = nullptr;
		}
		_this->_handler = nullptr;

		_this->_thread.Detach();

		LOGD("[MP] Client thread exited: {} ({})", NetworkManagerBase::ReasonToString(reason), reason);
	}

	void NetworkManagerBase::OnServerThread(void* param)
	{
		Thread::SetCurrentName("Multiplayer server");

		NetworkManagerBase* _this = static_cast<NetworkManagerBase*>(param);
		INetworkHandler* handler = _this->_handler;
		ENetHost* host = _this->_host;

		_this->_connectedPeers.reserve(16);

		ENetEvent ev{};
		while DEATH_LIKELY(_this->_state != NetworkState::None) {
			std::int32_t result;
			{
				std::unique_lock lock(_this->_lock);
				result = enet_host_service(host, &ev, 0);
			}

			if DEATH_UNLIKELY(result <= 0) {
				if DEATH_UNLIKELY(result < 0) {
					LOGE("[MP] enet_host_service() returned {}", result);

					// Server failed, try to recreate it
					SmallVector<Peer, 1> peersSnapshot;
					{
						std::unique_lock lock(_this->_lock);
						std::swap(peersSnapshot, _this->_connectedPeers);
					}
					for (const Peer& p : peersSnapshot) {
#		if defined(WITH_WEBSOCKET)
						if DEATH_LIKELY(!p.IsWebSocket())
#		endif
						{
							_this->OnPeerDisconnected(p, Reason::ConnectionLost);
						}
					}
					ENetAddress addr = host->address;
					{
						std::unique_lock lock(_this->_lock);
						enet_host_destroy(host);
						host = enet_host_create(&addr, MaxPeerCount, std::size_t(NetworkChannel::Count), 0, 0);
						_this->_host = host;
					}

					if (host == nullptr) {
						LOGE("Failed to recreate multiplayer server");
						break;
					}
				}
#		if defined(WITH_WEBSOCKET)
				_this->ProcessWsQueue(handler);
#		endif
				Thread::Sleep(ProcessingIntervalMs);
				continue;
			}

			switch (ev.type) {
				case ENET_EVENT_TYPE_CONNECT: {
					ConnectionResult result = _this->OnPeerConnected(ev.peer, ev.data);
					if DEATH_LIKELY(result.IsSuccessful()) {
						std::unique_lock lock(_this->_lock);
						bool alreadyExists = false;
						for (std::size_t i = 0; i < _this->_connectedPeers.size(); i++) {
							if (Peer(ev.peer) == _this->_connectedPeers[i]) {
								alreadyExists = true;
								break;
							}
						}
						if DEATH_UNLIKELY(alreadyExists) {
							LOGW("Peer is already connected [{:.8x}]", std::uint64_t(ev.peer));
						} else {
							_this->_connectedPeers.push_back(Peer(ev.peer));
						}
					} else {
						std::unique_lock lock(_this->_lock);
						enet_peer_disconnect(ev.peer, std::uint32_t(result.FailureReason));
					}
					break;
				}
				case ENET_EVENT_TYPE_RECEIVE: {
					// A valid packet carries at least the 1-byte packet-type prefix. Drop empty packets so the
					// data[0] read and exceptPrefix(1) slice below can't run out of bounds - a malicious or buggy
					// peer can legally send a zero-length packet, and exceptPrefix(1) on it would otherwise yield a
					// SIZE_MAX-length view in release builds (the bounds check is compiled out).
					if (ev.packet->dataLength >= 1) {
						auto data = arrayView(ev.packet->data, ev.packet->dataLength);
						handler->OnPacketReceived(ev.peer, ev.channelID, data[0], data.exceptPrefix(1));
					}
					enet_packet_destroy(ev.packet);
					break;
				}
				case ENET_EVENT_TYPE_DISCONNECT:
					_this->OnPeerDisconnected(ev.peer, Reason(ev.data));
					break;
				case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
					_this->OnPeerDisconnected(ev.peer, Reason::ConnectionLost);
					break;
			}

#		if defined(WITH_WEBSOCKET)
			_this->ProcessWsQueue(handler);
#		endif
		}

		{
			// Serialize the teardown with the main-thread send paths, which use _connectedPeers/_host under _lock
			std::unique_lock lock(_this->_lock);
			for (const Peer& p : _this->_connectedPeers) {
#		if defined(WITH_WEBSOCKET)
				if DEATH_LIKELY(!p.IsWebSocket())
#		endif
				{
					enet_peer_disconnect_now(p._enet, std::uint32_t(Reason::ServerStopped));
				}
			}
			_this->_connectedPeers.clear();

			enet_host_destroy(_this->_host);
			_this->_host = nullptr;
		}
		_this->_handler = nullptr;

#		if defined(WITH_WEBSOCKET)
		if (_this->_wsServer != nullptr) {
			_this->_wsServer->stop();
			_this->_wsServer = nullptr;
		}
		if (_this->_wsClient != nullptr) {
			_this->_wsClient->stop();
			_this->_wsClient = nullptr;
		}
		{
			std::unique_lock<Spinlock> lock(_this->_wsLock);
			_this->_wsPeers.clear();
			_this->_wsPendingEvents.clear();
		}
#		endif

		_this->_thread.Detach();

		LOGD("[MP] Server thread exited");
	}
#	endif

#endif

}

#endif