import { Socket } from "node:net";

export interface MinecraftServerProbe {
  online: boolean;
  playersOnline: number | null;
  playersMaximum: number | null;
}

export function encodeVarInt(input: number): Buffer {
  let value = input >>> 0;
  const bytes: number[] = [];
  do {
    let byte = value & 0x7f;
    value >>>= 7;
    if (value !== 0) byte |= 0x80;
    bytes.push(byte);
  } while (value !== 0 && bytes.length < 5);
  return Buffer.from(bytes);
}

function decodeVarInt(buffer: Buffer, offset: number): { value: number; bytes: number } | undefined {
  let value = 0;
  for (let index = 0; index < 5; index += 1) {
    const byte = buffer[offset + index];
    if (byte === undefined) return undefined;
    value |= (byte & 0x7f) << (7 * index);
    if ((byte & 0x80) === 0) return { value: value >>> 0, bytes: index + 1 };
  }
  throw new Error("VarInt Minecraft trop long");
}

function minecraftString(value: string): Buffer {
  const bytes = Buffer.from(value, "utf8");
  return Buffer.concat([encodeVarInt(bytes.length), bytes]);
}

export function buildStatusRequest(host: string, port: number): Buffer {
  const portBytes = Buffer.allocUnsafe(2);
  portBytes.writeUInt16BE(port);
  const handshakeBody = Buffer.concat([
    encodeVarInt(0),
    encodeVarInt(-1),
    minecraftString(host),
    portBytes,
    encodeVarInt(1),
  ]);
  const handshake = Buffer.concat([encodeVarInt(handshakeBody.length), handshakeBody]);
  return Buffer.concat([handshake, Buffer.from([0x01, 0x00])]);
}

export function parseStatusResponse(buffer: Buffer): MinecraftServerProbe | undefined {
  const packetLength = decodeVarInt(buffer, 0);
  if (!packetLength) return undefined;
  if (packetLength.value > 1024 * 1024) throw new Error("Réponse de statut Minecraft trop grande");
  if (buffer.length < packetLength.bytes + packetLength.value) return undefined;
  const packetId = decodeVarInt(buffer, packetLength.bytes);
  if (!packetId || packetId.value !== 0) throw new Error("Paquet de statut Minecraft invalide");
  const textLength = decodeVarInt(buffer, packetLength.bytes + packetId.bytes);
  if (!textLength || textLength.value > 1024 * 1024) throw new Error("JSON de statut Minecraft invalide");
  const start = packetLength.bytes + packetId.bytes + textLength.bytes;
  const end = start + textLength.value;
  if (end > buffer.length) return undefined;
  const status = JSON.parse(buffer.subarray(start, end).toString("utf8")) as {
    players?: { online?: unknown; max?: unknown };
  };
  const online = Number.isSafeInteger(status.players?.online) ? status.players!.online as number : null;
  const maximum = Number.isSafeInteger(status.players?.max) ? status.players!.max as number : null;
  return { online: true, playersOnline: online, playersMaximum: maximum };
}

export async function probeMinecraftServer(host: string, port: number): Promise<MinecraftServerProbe> {
  return new Promise((resolve) => {
    const socket = new Socket();
    let complete = false;
    let response = Buffer.alloc(0);
    const finish = (result: MinecraftServerProbe): void => {
      if (complete) return;
      complete = true;
      socket.destroy();
      resolve(result);
    };
    const offline = (): void => finish({ online: false, playersOnline: null, playersMaximum: null });
    socket.setTimeout(3_000);
    socket.once("connect", () => socket.write(buildStatusRequest(host, port)));
    socket.on("data", (chunk: Buffer) => {
      response = Buffer.concat([response, chunk]);
      if (response.length > 1024 * 1024 + 16) return offline();
      try {
        const parsed = parseStatusResponse(response);
        if (parsed) finish(parsed);
      } catch {
        offline();
      }
    });
    socket.once("timeout", offline);
    socket.once("error", offline);
    socket.once("end", () => { if (!complete) offline(); });
    socket.connect(port, host);
  });
}
