import { API_RETURNCODES, defaultNetworkTimeout, type TNetworkResponse } from '@hizi.io/engine-sdk';

type Envelope = {
  requestId?: string;
  status?: number;
  headers?: Record<string, string>;
  reply?: Record<string, unknown>;
  event?: string;
  payload?: unknown;
};

// One connection carries both requests and server events. This replaces enableWebSockets.
export class GameSocket {
  private nextId = 0;
  private pending = new Map<string, {
    resolve: (value: TNetworkResponse<unknown>) => void;
    timer: ReturnType<typeof setTimeout>;
  }>();

  private constructor(private socket: WebSocket, onEvent: (event: string, payload: unknown) => void) {
    socket.addEventListener('message', message => {
      const frame = typeof message.data === 'string' ? message.data
        : message.data instanceof ArrayBuffer ? new TextDecoder('utf-8').decode(message.data)
        : null;
      if (frame === null) return;
      let data: Envelope;
      try { data = JSON.parse(frame); } catch { return; }
      if (!data || typeof data !== 'object') return;
      // Crash events can arrive before the matching placeBet reply.
      if (typeof data.event === 'string') { onEvent(data.event, data.payload); return; }
      if (!data.requestId) return;
      const pending = this.pending.get(data.requestId);
      if (!pending) return;
      this.pending.delete(data.requestId);
      clearTimeout(pending.timer);
      if (typeof data.status === 'number' && data.status >= 200 && data.status < 300) {
        pending.resolve({ success: true, result: data.reply ?? {} });
        return;
      }
      const headers = Object.fromEntries(Object.entries(data.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v]));
      let text = headers['x-h-error-msg'];
      const encoded = headers['x-h-error-msg-base64'];
      if (encoded) {
        try { text = new TextDecoder().decode(Uint8Array.from(atob(encoded), c => c.charCodeAt(0))); }
        catch { text = text ?? 'Invalid error text'; }
      }
      pending.resolve({ success: false, error: {
        code: String(headers['x-h-error-id'] ?? data.reply?.errorId ?? data.reply?.code ?? API_RETURNCODES.UNEXPECTED),
        message: String(text ?? data.reply?.errorText ?? data.reply?.message ?? 'Request failed'),
        ...(data.reply?.passThroughData === undefined ? {} : { passThroughData: data.reply.passThroughData }),
      } });
    });
    socket.addEventListener('close', () => this.failPending());
    socket.addEventListener('error', () => this.failPending());
  }

  static async connect(url: string, onEvent: (event: string, payload: unknown) => void): Promise<GameSocket> {
    const socket = new WebSocket(url);
    // The router sends replies as binary. Without this the browser delivers a Blob,
    // String(blob) is "[object Blob]", and every reply and event is discarded.
    socket.binaryType = 'arraybuffer';
    const transport = new GameSocket(socket, onEvent);
    await new Promise<void>((resolve, reject) => {
      const timer = setTimeout(() => { socket.close(); reject(new Error('Socket connection timed out')); }, defaultNetworkTimeout);
      const failed = () => { clearTimeout(timer); reject(new Error('Socket connection failed')); };
      socket.addEventListener('error', failed, { once: true });
      socket.addEventListener('close', failed, { once: true });
      socket.addEventListener('open', () => {
        clearTimeout(timer);
        socket.removeEventListener('error', failed);
        socket.removeEventListener('close', failed);
        resolve();
      }, { once: true });
    });
    return transport;
  }

  async request<T>(body: Record<string, unknown>): Promise<TNetworkResponse<T>> {
    if (this.socket.readyState !== WebSocket.OPEN) return this.networkError();
    const requestId = `frontend-${++this.nextId}`;
    return new Promise(resolve => {
      const timer = setTimeout(() => {
        this.pending.delete(requestId);
        resolve(this.networkError());
      }, defaultNetworkTimeout);
      this.pending.set(requestId, { timer, resolve: value => resolve(value as TNetworkResponse<T>) });
      try {
        this.socket.send(JSON.stringify({ cmd: 'game', data: {
          body, requestId, method: 'POST', headers: { 'Content-Type': 'application/json' },
        } }));
      } catch {
        clearTimeout(timer);
        this.pending.delete(requestId);
        resolve(this.networkError());
      }
    });
  }

  private networkError(): TNetworkResponse<never> {
    return { success: false, error: {
      code: String(API_RETURNCODES.NETWORKERROR),
      message: 'Connection interrupted. Reconcile the round before another action.',
    } };
  }

  private failPending(): void {
    for (const pending of this.pending.values()) {
      clearTimeout(pending.timer);
      pending.resolve(this.networkError());
    }
    this.pending.clear();
  }

  close(): void { this.failPending(); this.socket.close(); }
}
