import * as plugins from '../../plugins.js';
import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallResult } from '../../core/index.js';
import { MinecraftServerMapper } from './minecraft_server.mapper.js';
import type { IMinecraftServerConfig, IMinecraftServerJavaStatus, TMinecraftServerType } from './minecraft_server.types.js';
import { minecraftServerDefaultPort, minecraftServerDefaultTimeoutMs, minecraftServerProfile } from './minecraft_server.types.js';

interface IMinecraftServerEndpoint {
  host: string;
  port: number;
}

export class MinecraftServerClient extends SimpleLocalClient<IMinecraftServerConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly clientConfig: IMinecraftServerConfig) {
    super(minecraftServerProfile, clientConfig);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.cachedSnapshot) {
      return clone(this.cachedSnapshot);
    }

    if (!forceRefreshArg && this.clientConfig.snapshot) {
      this.cachedSnapshot = MinecraftServerMapper.toSnapshot({ config: this.clientConfig, source: 'snapshot', online: this.clientConfig.snapshot.online });
      return clone(this.cachedSnapshot);
    }

    if (this.hasEndpoint()) {
      try {
        const status = await this.fetchJavaStatus();
        this.cachedSnapshot = MinecraftServerMapper.toSnapshot({ config: this.clientConfig, rawData: this.rawDataFromStatus(status), online: true, source: 'tcp' });
      } catch (errorArg) {
        this.cachedSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return clone(this.cachedSnapshot);
    }

    if (this.clientConfig.client || this.clientConfig.snapshotProvider) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return clone(this.cachedSnapshot);
    }

    if (this.hasManualData()) {
      this.cachedSnapshot = MinecraftServerMapper.toSnapshot({ config: this.clientConfig, rawData: this.rawData(), online: this.clientConfig.online ?? true, source: 'manual' });
      return clone(this.cachedSnapshot);
    }

    this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
    return clone(this.cachedSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const liveCapable = this.hasEndpoint() || Boolean(this.clientConfig.client || this.clientConfig.snapshotProvider);
    const snapshot = await this.getSnapshot(liveCapable);
    const success = liveCapable && snapshot.online && !snapshot.error && snapshot.source === 'tcp';
    return { success, error: success ? undefined : snapshot.error || 'Minecraft Server refresh requires a reachable direct Java Edition TCP endpoint, injected client, or snapshotProvider.', data: snapshot };
  }

  public async destroy(): Promise<void> {
    await this.clientConfig.client?.destroy?.();
  }

  private async fetchJavaStatus(): Promise<IMinecraftServerJavaStatus> {
    const serverType = this.serverType();
    if (serverType !== 'Java Edition') {
      throw new Error(`Minecraft Server live polling supports direct Java Edition TCP status only; ${serverType} requires mcstatus-compatible protocol support.`);
    }
    const endpoint = this.requireEndpoint();
    const timeoutMs = this.clientConfig.timeoutMs || minecraftServerDefaultTimeoutMs;
    const protocolVersion = numberValue(this.clientConfig.protocolVersion) ?? -1;
    const startedAt = Date.now();

    const response = await new Promise<Record<string, unknown>>((resolve, reject) => {
      let buffer = Buffer.alloc(0);
      let settled = false;
      const socket = plugins.net.createConnection({ host: endpoint.host, port: endpoint.port });
      const timer = setTimeout(() => finish(new Error(`Minecraft Server TCP status request to ${endpoint.host}:${endpoint.port} timed out after ${timeoutMs}ms.`)), timeoutMs);

      const finish = (errorArg?: Error, responseArg?: Record<string, unknown>) => {
        if (settled) {
          return;
        }
        settled = true;
        clearTimeout(timer);
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(responseArg || {});
      };

      socket.on('connect', () => {
        socket.write(Buffer.concat([
          createHandshakePacket(endpoint.host, endpoint.port, protocolVersion),
          createPacket(Buffer.from([0x00])),
          createPingPacket(BigInt(Date.now())),
        ]));
      });
      socket.on('data', (chunkArg) => {
        buffer = Buffer.concat([buffer, Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)]);
        try {
          const parsed = parseStatusPacket(buffer);
          if (parsed) {
            finish(undefined, parsed);
          }
        } catch (errorArg) {
          finish(errorArg instanceof Error ? errorArg : new Error(String(errorArg)));
        }
      });
      socket.on('error', (errorArg) => finish(errorArg));
      socket.on('close', () => {
        if (!settled) {
          finish(new Error(`Minecraft Server TCP connection to ${endpoint.host}:${endpoint.port} closed before a status response was received.`));
        }
      });
    });

    const players = objectValue(response.players) || {};
    const version = objectValue(response.version) || {};
    const sample = Array.isArray(players.sample) ? players.sample : [];
    return {
      latency: Date.now() - startedAt,
      motd: textComponentToPlain(response.description),
      players_max: numberValue(players.max) ?? 0,
      players_online: numberValue(players.online) ?? 0,
      protocol_version: numberValue(version.protocol) ?? 0,
      version: stringValue(version.name) || 'Old',
      players_list: sample.map((playerArg) => stringValue(objectValue(playerArg)?.name)).filter((nameArg): nameArg is string => Boolean(nameArg)).sort(),
      rawResponse: response,
    };
  }

  private rawDataFromStatus(statusArg: IMinecraftServerJavaStatus): Record<string, unknown> {
    const endpoint = this.requireEndpoint();
    const name = this.clientConfig.name || this.clientConfig.address || `${endpoint.host}:${endpoint.port}`;
    return {
      device: {
        id: this.clientConfig.uniqueId || this.clientConfig.deviceId || `${endpoint.host}:${endpoint.port}`,
        name,
        manufacturer: minecraftServerProfile.manufacturer,
        model: 'Java Edition server',
        host: endpoint.host,
        port: endpoint.port,
        protocol: 'tcp',
        configurationUrl: `minecraft://${endpoint.host}:${endpoint.port}`,
      },
      entities: [
        { id: 'status', name: 'Status', platform: 'binary_sensor', state: true, attributes: { deviceClass: 'connectivity', serverType: 'Java Edition' } },
        { id: 'version', name: 'Version', platform: 'sensor', state: statusArg.version, attributes: { serverType: 'Java Edition' } },
        { id: 'protocol_version', name: 'Protocol Version', platform: 'sensor', state: statusArg.protocol_version, attributes: { entityCategory: 'diagnostic' } },
        { id: 'players_max', name: 'Players Max', platform: 'sensor', state: statusArg.players_max, unit: 'players' },
        { id: 'latency', name: 'Latency', platform: 'sensor', state: statusArg.latency, unit: 'ms', attributes: { entityCategory: 'diagnostic' } },
        { id: 'motd', name: 'MOTD', platform: 'sensor', state: statusArg.motd },
        { id: 'players_online', name: 'Players Online', platform: 'sensor', state: statusArg.players_online, unit: 'players', attributes: { players_list: statusArg.players_list || [] } },
      ],
      status: statusArg,
      online: true,
      fetchedAt: new Date().toISOString(),
    };
  }

  private requireEndpoint(): IMinecraftServerEndpoint {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Minecraft Server live polling requires config.address, config.host, or config.url.');
    }
    return endpoint;
  }

  private endpoint(): IMinecraftServerEndpoint | undefined {
    const address = stringValue(this.clientConfig.address) || stringValue(this.clientConfig.url);
    if (address) {
      return parseEndpoint(address, this.clientConfig.port);
    }
    const host = stringValue(this.clientConfig.host);
    if (!host) {
      return undefined;
    }
    return { host, port: this.clientConfig.port || minecraftServerDefaultPort };
  }

  private serverType(): TMinecraftServerType {
    const value = stringValue(this.clientConfig.serverType) || stringValue(this.clientConfig.type) || 'Java Edition';
    if (value === 'Bedrock Edition' || value === 'Legacy Java Edition') {
      return value;
    }
    return 'Java Edition';
  }

  private hasEndpoint(): boolean {
    return Boolean(this.endpoint());
  }

  private hasManualData(): boolean {
    return this.rawData() !== undefined || Boolean(this.clientConfig.entities?.length || this.clientConfig.snapshot);
  }

  private rawData(): unknown {
    return this.clientConfig.rawData ?? this.clientConfig.rawText;
  }

  private createOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return MinecraftServerMapper.toSnapshot({ config: this.clientConfig, online: false, source: 'runtime', error: errorArg });
  }
}

const createHandshakePacket = (hostArg: string, portArg: number, protocolVersionArg: number): Buffer => {
  const port = Buffer.alloc(2);
  port.writeUInt16BE(portArg, 0);
  return createPacket(Buffer.concat([
    encodeVarInt(0),
    encodeVarInt(protocolVersionArg),
    encodeString(hostArg),
    port,
    encodeVarInt(1),
  ]));
};

const createPingPacket = (payloadArg: bigint): Buffer => {
  const payload = Buffer.alloc(8);
  payload.writeBigInt64BE(payloadArg);
  return createPacket(Buffer.concat([encodeVarInt(1), payload]));
};

const createPacket = (payloadArg: Buffer): Buffer => Buffer.concat([encodeVarInt(payloadArg.length), payloadArg]);

const encodeString = (valueArg: string): Buffer => {
  const value = Buffer.from(valueArg, 'utf8');
  return Buffer.concat([encodeVarInt(value.length), value]);
};

const encodeVarInt = (valueArg: number): Buffer => {
  let value = valueArg >>> 0;
  const bytes: number[] = [];
  while (true) {
    if ((value & ~0x7f) === 0) {
      bytes.push(value);
      break;
    }
    bytes.push((value & 0x7f) | 0x80);
    value >>>= 7;
  }
  return Buffer.from(bytes);
};

const parseStatusPacket = (bufferArg: Buffer): Record<string, unknown> | undefined => {
  const length = readVarInt(bufferArg, 0);
  if (!length) {
    return undefined;
  }
  if (length.value > 2 * 1024 * 1024) {
    throw new Error('Minecraft Server status response exceeded 2 MiB.');
  }
  const packetEnd = length.offset + length.value;
  if (bufferArg.length < packetEnd) {
    return undefined;
  }
  const payload = bufferArg.subarray(length.offset, packetEnd);
  const packetId = readVarInt(payload, 0);
  if (!packetId) {
    return undefined;
  }
  if (packetId.value !== 0) {
    throw new Error(`Minecraft Server returned unexpected status packet id ${packetId.value}.`);
  }
  const response = readString(payload, packetId.offset);
  if (!response) {
    return undefined;
  }
  const parsed = JSON.parse(response.value) as unknown;
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
    throw new Error('Minecraft Server status response was not a JSON object.');
  }
  return parsed as Record<string, unknown>;
};

const readString = (bufferArg: Buffer, offsetArg: number): { value: string; offset: number } | undefined => {
  const length = readVarInt(bufferArg, offsetArg);
  if (!length) {
    return undefined;
  }
  const end = length.offset + length.value;
  if (bufferArg.length < end) {
    return undefined;
  }
  return { value: bufferArg.subarray(length.offset, end).toString('utf8'), offset: end };
};

const readVarInt = (bufferArg: Buffer, offsetArg: number): { value: number; offset: number } | undefined => {
  let value = 0;
  let position = 0;
  for (let index = offsetArg; index < Math.min(bufferArg.length, offsetArg + 5); index++) {
    const byte = bufferArg[index];
    value |= (byte & 0x7f) << position;
    if ((byte & 0x80) === 0) {
      return { value, offset: index + 1 };
    }
    position += 7;
  }
  if (bufferArg.length >= offsetArg + 5) {
    throw new Error('Minecraft Server response contained an invalid VarInt.');
  }
  return undefined;
};

const parseEndpoint = (valueArg: string, portArg?: number): IMinecraftServerEndpoint => {
  if (/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    const url = new URL(valueArg);
    return { host: url.hostname, port: portArg || (url.port ? Number(url.port) : minecraftServerDefaultPort) };
  }
  const bracketMatch = /^\[([^\]]+)](?::(\d+))?$/.exec(valueArg);
  if (bracketMatch) {
    return { host: bracketMatch[1], port: portArg || numberValue(bracketMatch[2]) || minecraftServerDefaultPort };
  }
  const colonCount = (valueArg.match(/:/g) || []).length;
  if (colonCount === 1) {
    const [host, port] = valueArg.split(':', 2);
    return { host, port: portArg || numberValue(port) || minecraftServerDefaultPort };
  }
  return { host: valueArg, port: portArg || minecraftServerDefaultPort };
};

const textComponentToPlain = (valueArg: unknown): string => stripMinecraftFormatting(textComponent(valueArg).trim());

const textComponent = (valueArg: unknown): string => {
  if (valueArg === null || valueArg === undefined) {
    return '';
  }
  if (typeof valueArg === 'string' || typeof valueArg === 'number' || typeof valueArg === 'boolean') {
    return String(valueArg);
  }
  if (Array.isArray(valueArg)) {
    return valueArg.map((itemArg) => textComponent(itemArg)).join('');
  }
  const object = objectValue(valueArg);
  if (!object) {
    return '';
  }
  const parts = [textPart(object.text), textPart(object.translate), textComponent(object.with), textComponent(object.extra)];
  return parts.filter(Boolean).join('');
};

const stripMinecraftFormatting = (valueArg: string): string => valueArg.replace(/§[0-9A-FK-OR]/gi, '');

const objectValue = (valueArg: unknown): Record<string, unknown> | undefined => {
  return valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;
};

const textPart = (valueArg: unknown): string | undefined => {
  return typeof valueArg === 'string' ? valueArg : stringValue(valueArg);
};

const stringValue = (valueArg: unknown): string | undefined => {
  return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
};

const numberValue = (valueArg: unknown): number | undefined => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return Math.round(valueArg);
  }
  if (typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg))) {
    return Math.round(Number(valueArg));
  }
  return undefined;
};

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
