<?php

namespace App\Services;

use RuntimeException;

class MinecraftRconClient
{
    private const AUTH_PACKET_TYPE = 3;

    private const COMMAND_PACKET_TYPE = 2;

    /**
     * @var resource|null
     */
    private $socket = null;

    private int $requestId = 1;

    public function command(string $command): string
    {
        $this->connect();

        try {
            $this->authenticate();

            $requestId = $this->nextRequestId();
            $this->writePacket($requestId, self::COMMAND_PACKET_TYPE, $command);

            $response = $this->readPacket();

            if ($response['id'] !== $requestId) {
                throw new RuntimeException('Unexpected RCON response id.');
            }

            return $response['body'];
        } finally {
            $this->disconnect();
        }
    }

    private function connect(): void
    {
        $host = config('rcon.host');
        $port = (int) config('rcon.port');
        $timeout = (float) config('rcon.timeout');

        if (blank($host)) {
            throw new RuntimeException('MC_RCON_HOST is not configured.');
        }

        $socket = @stream_socket_client(
            "tcp://{$host}:{$port}",
            $errorCode,
            $errorMessage,
            $timeout,
        );

        if ($socket === false) {
            throw new RuntimeException("Unable to connect to Minecraft RCON: {$errorMessage} ({$errorCode}).");
        }

        $this->socket = $socket;
        $seconds = (int) floor($timeout);
        $microseconds = (int) (($timeout - $seconds) * 1_000_000);

        stream_set_timeout($this->socket, $seconds, $microseconds);
    }

    private function authenticate(): void
    {
        $password = config('rcon.password');

        if (blank($password)) {
            throw new RuntimeException('MC_RCON_PASSWORD is not configured.');
        }

        $requestId = $this->nextRequestId();
        $this->writePacket($requestId, self::AUTH_PACKET_TYPE, (string) $password);

        do {
            $response = $this->readPacket();
        } while ($response['type'] !== self::COMMAND_PACKET_TYPE);

        if ($response['id'] === -1 || $response['id'] !== $requestId) {
            throw new RuntimeException('Minecraft RCON authentication failed.');
        }
    }

    private function disconnect(): void
    {
        if (is_resource($this->socket)) {
            fclose($this->socket);
        }

        $this->socket = null;
    }

    private function nextRequestId(): int
    {
        return $this->requestId++;
    }

    private function writePacket(int $requestId, int $type, string $body): void
    {
        $payload = pack('VV', $requestId, $type).$body."\x00\x00";
        $packet = pack('V', strlen($payload)).$payload;
        $bytesWritten = 0;

        while ($bytesWritten < strlen($packet)) {
            $written = fwrite($this->socket, substr($packet, $bytesWritten));

            if ($written === false || $written === 0) {
                throw new RuntimeException('Unable to write RCON packet.');
            }

            $bytesWritten += $written;
        }
    }

    /**
     * @return array{id: int, type: int, body: string}
     */
    private function readPacket(): array
    {
        $sizeData = $this->readBytes(4);
        $size = unpack('V', $sizeData)[1];

        if ($size < 10) {
            throw new RuntimeException('Invalid RCON packet size.');
        }

        $packet = $this->readBytes($size);
        $header = unpack('Vid/Vtype', substr($packet, 0, 8));
        $id = $header['id'] >= 0x80000000
            ? $header['id'] - 0x100000000
            : $header['id'];

        return [
            'id' => $id,
            'type' => $header['type'],
            'body' => substr($packet, 8, -2),
        ];
    }

    private function readBytes(int $length): string
    {
        $data = '';

        while (strlen($data) < $length) {
            $chunk = fread($this->socket, $length - strlen($data));

            if ($chunk === false || $chunk === '') {
                throw new RuntimeException('Unable to read RCON packet.');
            }

            $data .= $chunk;
        }

        return $data;
    }
}
