using System.Buffers.Binary;
using System.Net.Sockets;
using System.Text;

namespace BrassAndSigil.Server.Services;

/// <summary>
/// Minimal Minecraft RCON client (Source RCON protocol).
/// Used for sending console commands and reading "list" output for player counts.
/// </summary>
public sealed class RconClient : IDisposable
{
    private const int SERVERDATA_AUTH = 3;
    private const int SERVERDATA_EXECCOMMAND = 2;
    private const int SERVERDATA_RESPONSE_VALUE = 0;

    private TcpClient? _tcp;
    private NetworkStream? _stream;
    private int _nextRequestId = 1;

    public bool Connected => _tcp?.Connected ?? false;

    public async Task ConnectAsync(string host, int port, string password, CancellationToken ct = default)
    {
        _tcp = new TcpClient();
        await _tcp.ConnectAsync(host, port, ct);
        _stream = _tcp.GetStream();

        var authId = NextId();
        await SendPacketAsync(authId, SERVERDATA_AUTH, password, ct);

        // Read auth response. Server sends an empty response value first, then the auth result.
        var (id1, _, _) = await ReadPacketAsync(ct);
        if (id1 == -1) throw new InvalidOperationException("RCON auth failed (bad password)");
        // Auth ok if id matches what we sent
    }

    public async Task<string> SendCommandAsync(string command, CancellationToken ct = default)
    {
        if (_stream == null) throw new InvalidOperationException("Not connected");
        var id = NextId();
        await SendPacketAsync(id, SERVERDATA_EXECCOMMAND, command, ct);
        var (_, _, body) = await ReadPacketAsync(ct);
        return body;
    }

    private int NextId() => Interlocked.Increment(ref _nextRequestId);

    private async Task SendPacketAsync(int id, int type, string body, CancellationToken ct)
    {
        var bodyBytes = Encoding.UTF8.GetBytes(body);
        var packetSize = 4 + 4 + bodyBytes.Length + 2; // id + type + body + 2 null bytes
        var buffer = new byte[4 + packetSize];
        BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), packetSize);
        BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(4, 4), id);
        BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(8, 4), type);
        bodyBytes.CopyTo(buffer.AsSpan(12));
        // last two bytes already 0 by default
        await _stream!.WriteAsync(buffer, ct);
    }

    private async Task<(int Id, int Type, string Body)> ReadPacketAsync(CancellationToken ct)
    {
        var sizeBuf = new byte[4];
        await ReadExactAsync(sizeBuf, ct);
        var size = BinaryPrimitives.ReadInt32LittleEndian(sizeBuf);
        if (size < 10 || size > 1024 * 1024) throw new InvalidOperationException($"Bad RCON packet size {size}");

        var pkt = new byte[size];
        await ReadExactAsync(pkt, ct);
        var id = BinaryPrimitives.ReadInt32LittleEndian(pkt.AsSpan(0, 4));
        var type = BinaryPrimitives.ReadInt32LittleEndian(pkt.AsSpan(4, 4));
        var body = Encoding.UTF8.GetString(pkt, 8, size - 10); // strip 2 trailing nulls
        return (id, type, body);
    }

    private async Task ReadExactAsync(byte[] buffer, CancellationToken ct)
    {
        var read = 0;
        while (read < buffer.Length)
        {
            var n = await _stream!.ReadAsync(buffer.AsMemory(read), ct);
            if (n == 0) throw new EndOfStreamException();
            read += n;
        }
    }

    public void Dispose()
    {
        _stream?.Dispose();
        _tcp?.Dispose();
    }
}
