/**
 * Socket.IO protocol encoder / decoder.
 *
 * Wire format subset used by imks:
 * Connect: 0[/namespace,][data]
 * Disconnect: 1[/namespace,]
 * Event: 2[/namespace,][ackId][eventData]
 * Ack: 3[/namespace,]ackId[data]
 * ConnectError: 4[/namespace,]message
 * BinaryEvent: 5[attachmentCount-][/namespace,...]
 * BinaryAck: 6[attachmentCount-][/namespace,...]
 */

const EP_OPEN = '0';
const EP_CLOSE = '1';
const EP_PING = '2';
const EP_PONG = '3';
const EP_MESSAGE = '4';

const SP_CONNECT = '0';
const SP_DISCONNECT = '1';
const SP_EVENT = '2';
const SP_BINARY_EVENT = '5';
const SP_BINARY_ACK = '6';

/** Encode an Engine.IO packet to a string payload. */
export function encodeEnginePacket(type: string, data?: string | Uint8Array): string {
  if (data === undefined || (typeof data === 'string' && data.length === 0)) {
    return type;
  }
  return type + (typeof data === 'string' ? data : '');
}

/** Decode an Engine.IO packet string. */
export function decodeEnginePacket(raw: string): { type: string; data: string } {
  if (raw.length === 0) {
    return { type: '', data: '' };
  }
  return { type: raw[0], data: raw.slice(1) };
}

/** Encode a Socket.IO event packet. */
export function encodeSocketEvent(namespace: string, eventName: string, data: unknown): string {
  const nsPart = namespace !== '/' ? `${namespace},` : '';
  const eventArr = [eventName, data];
  const json = JSON.stringify(eventArr);
  return `${SP_EVENT}${nsPart}${json}`;
}

/** Encode a Socket.IO event with an ack id. */
export function encodeSocketEventAck(
  namespace: string,
  eventName: string,
  data: unknown,
  ackId: number,
): string {
  const nsPart = namespace !== '/' ? `${namespace},` : '';
  const eventArr = [eventName, data];
  const json = JSON.stringify(eventArr);
  return `${SP_EVENT}${nsPart}${ackId}${json}`;
}

/** Encode a Socket.IO connect packet. */
export function encodeSocketConnect(namespace: string, authData?: Record<string, unknown>): string {
  if (namespace === '/' && !authData) {
    return SP_CONNECT;
  }
  const nsPart = namespace !== '/' ? `${namespace},` : '';
  const data = authData ? JSON.stringify(authData) : '';
  return `${SP_CONNECT}${nsPart}${data}`;
}

/** Encode a Socket.IO disconnect packet. */
export function encodeSocketDisconnect(namespace: string): string {
  const nsPart = namespace !== '/' ? `${namespace},` : '';
  return `${SP_DISCONNECT}${nsPart}`;
}

/** Encode a full Engine.IO message containing a Socket.IO CONNECT packet. */
export function encodeOutgoingConnect(
  namespace: string,
  authData?: Record<string, unknown>,
): string {
  return encodeEnginePacket(EP_MESSAGE, encodeSocketConnect(namespace, authData));
}

/** Encode a full Engine.IO message containing a Socket.IO DISCONNECT packet. */
export function encodeOutgoingDisconnect(namespace: string): string {
  return encodeEnginePacket(EP_MESSAGE, encodeSocketDisconnect(namespace));
}

/** Encode a full Engine.IO message containing a Socket.IO EVENT packet. */
export function encodeOutgoing(namespace: string, eventName: string, data: unknown): string {
  const socketPayload = encodeSocketEvent(namespace, eventName, data);
  return encodeEnginePacket(EP_MESSAGE, socketPayload);
}

export interface DecodedSocketPacket {
  type: string;
  namespace: string;
  event?: string;
  data?: unknown;
  id?: number;
}

/** Decode a Socket.IO packet string. */
export function decodeSocketPacket(raw: string): DecodedSocketPacket | null {
  if (raw.length === 0) return null;

  const typeChar = raw[0];
  let rest = raw.slice(1);

  if ((typeChar === SP_BINARY_EVENT || typeChar === SP_BINARY_ACK) && rest.length > 0) {
    const dashIdx = rest.indexOf('-');
    if (dashIdx > 0) {
      const countStr = rest.slice(0, dashIdx);
      const parsed = Number.parseInt(countStr, 10);
      if (!Number.isNaN(parsed)) {
        rest = rest.slice(dashIdx + 1);
      }
    }
  }

  let namespace = '/';
  if (rest.startsWith('/')) {
    const commaIdx = rest.indexOf(',');
    if (commaIdx > 0) {
      namespace = rest.slice(0, commaIdx);
      rest = rest.slice(commaIdx + 1);
    } else {
      const afterSlash = rest.slice(1);
      if (
        afterSlash === '' ||
        afterSlash.startsWith('[') ||
        afterSlash.startsWith('{') ||
        /^\d/.test(afterSlash)
      ) {
        rest = afterSlash;
      } else {
        namespace = rest;
        rest = '';
      }
    }
  }

  let id: number | undefined;
  let dataStr = rest;
  const idMatch = rest.match(/^(\d+)/);
  if (idMatch) {
    id = Number.parseInt(idMatch[1], 10);
    dataStr = rest.slice(idMatch[0].length);
  }

  let event: string | undefined;
  let data: unknown;

  if (dataStr.length > 0) {
    try {
      const parsed = JSON.parse(dataStr);
      if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === 'string') {
        event = parsed[0];
        data = parsed.length > 1 ? parsed[1] : undefined;
      } else {
        data = parsed;
      }
    } catch {
      data = dataStr;
    }
  }

  return {
    type: typeChar,
    namespace,
    event,
    data,
    id,
  };
}

export function encodePing(): string {
  return EP_PING;
}

export function encodePong(): string {
  return EP_PONG;
}

export function isPing(raw: string): boolean {
  return raw.startsWith(EP_PING);
}

export function isPong(raw: string): boolean {
  return raw.startsWith(EP_PONG);
}

export function isOpen(raw: string): boolean {
  return raw.startsWith(EP_OPEN);
}

export function isMessage(raw: string): boolean {
  return raw.startsWith(EP_MESSAGE);
}

export function isClose(raw: string): boolean {
  return raw.startsWith(EP_CLOSE);
}
