using System.Buffers;
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using System.Text;
using Phantom.Utils.Logging;
using Serilog;

namespace Phantom.Agent.Minecraft.Server;

public sealed class ServerStatusProtocol {
	private readonly ILogger logger;

	public ServerStatusProtocol(string loggerName) {
		this.logger = PhantomLogger.Create<ServerStatusProtocol>(loggerName);
	}

	public async Task<int?> GetOnlinePlayerCount(int serverPort, CancellationToken cancellationToken) {
		try {
			return await GetOnlinePlayerCountOrThrow(serverPort, cancellationToken);
		} catch (Exception e) {
			logger.Error(e, "Caught exception while checking if players are online.");
			return null;
		}
	}

	private async Task<int?> GetOnlinePlayerCountOrThrow(int serverPort, CancellationToken cancellationToken) {
		using var tcpClient = new TcpClient();
		await tcpClient.ConnectAsync(IPAddress.Loopback, serverPort, cancellationToken);
		var tcpStream = tcpClient.GetStream();
		
		// https://wiki.vg/Server_List_Ping
		tcpStream.WriteByte(0xFE);
		await tcpStream.FlushAsync(cancellationToken);

		short? messageLength = await ReadStreamHeader(tcpStream, cancellationToken);
		return messageLength == null ? null : await ReadOnlinePlayerCount(tcpStream, messageLength.Value * 2, cancellationToken);
	}

	private async Task<short?> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) {
		var headerBuffer = ArrayPool<byte>.Shared.Rent(3);
		try {
			await tcpStream.ReadExactlyAsync(headerBuffer, 0, 3, cancellationToken);

			if (headerBuffer[0] != 0xFF) {
				logger.Error("Unexpected first byte in response from server: {FirstByte}.", headerBuffer[0]);
				return null;
			}

			short messageLength = BinaryPrimitives.ReadInt16BigEndian(headerBuffer.AsSpan(1));
			if (messageLength <= 0) {
				logger.Error("Unexpected message length in response from server: {MessageLength}.", messageLength);
				return null;
			}
			
			return messageLength;
		} finally {
			ArrayPool<byte>.Shared.Return(headerBuffer);
		}
	}

	private async Task<int?> ReadOnlinePlayerCount(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) {
		var messageBuffer = ArrayPool<byte>.Shared.Rent(messageLength);
		try {
			await tcpStream.ReadExactlyAsync(messageBuffer, 0, messageLength, cancellationToken);

			// Valid response separator encoded in UTF-16BE is 0x00 0xA7 (ยง).
			const byte SeparatorSecondByte = 0xA7;
			
			static bool IsValidSeparator(ReadOnlySpan<byte> buffer, int index) {
				return index > 0 && buffer[index - 1] == 0x00;
			}
			
			int separator2 = Array.LastIndexOf(messageBuffer, SeparatorSecondByte);
			int separator1 = separator2 == -1 ? -1 : Array.LastIndexOf(messageBuffer, SeparatorSecondByte, separator2 - 1);
			if (!IsValidSeparator(messageBuffer, separator1) || !IsValidSeparator(messageBuffer, separator2)) {
				logger.Error("Could not find message separators in response from server.");
				return null;
			}

			string onlinePlayerCountStr = Encoding.BigEndianUnicode.GetString(messageBuffer.AsSpan((separator1 + 1)..(separator2 - 1)));
			if (!int.TryParse(onlinePlayerCountStr, out int onlinePlayerCount)) {
				logger.Error("Could not parse online player count in response from server: {OnlinePlayerCount}.", onlinePlayerCountStr);
				return null;
			}
			
			logger.Debug("Detected {OnlinePlayerCount} online player(s).", onlinePlayerCount);
			return onlinePlayerCount;
		} finally {
			ArrayPool<byte>.Shared.Return(messageBuffer);
		}
	}
}