const DEFAULT_TIMEOUT_MS = 12000;
const runtimeTimers = globalThis;

function buildWsUrl(raw) {
  const value = String(raw || '').trim();
  if (!value) return 'wss://shineup.me/ws';
  if (value.startsWith('ws://') || value.startsWith('wss://')) return value;
  if (value.startsWith('http://') || value.startsWith('https://')) {
    const parsed = new URL(value);
    parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
    if (!parsed.pathname || parsed.pathname === '/') parsed.pathname = '/ws';
    return parsed.toString();
  }
  return value;
}

function createRequestId(op) {
  return `${op}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

export class WsJsonClient {
  constructor(url) {
    this.url = buildWsUrl(url);
    this.ws = null;
    this.openPromise = null;
    this.pending = new Map();
    this.listeners = new Map();
  }

  async open() {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
    if (this.openPromise) return this.openPromise;

    this.openPromise = new Promise((resolve, reject) => {
      const ws = new WebSocket(this.url);
      this.ws = ws;

      ws.addEventListener('open', () => resolve(), { once: true });
      ws.addEventListener('error', () => reject(new Error(`Не удалось подключиться к ${this.url}`)), { once: true });
      ws.addEventListener('close', () => this.failPending('WebSocket соединение закрыто'));
      ws.addEventListener('message', (event) => this.handleMessage(event.data));
    }).finally(() => {
      this.openPromise = null;
    });

    return this.openPromise;
  }

  async request(op, payload = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
    await this.open();
    const requestId = createRequestId(op);
    const body = { op, requestId, payload };

    const response = new Promise((resolve, reject) => {
      const timer = runtimeTimers.setTimeout(() => {
        this.pending.delete(requestId);
        reject(new Error(`Таймаут ответа для операции ${op}`));
      }, timeoutMs);
      this.pending.set(requestId, {
        resolve: (value) => {
          runtimeTimers.clearTimeout(timer);
          resolve(value);
        },
        reject: (error) => {
          runtimeTimers.clearTimeout(timer);
          reject(error);
        },
      });
    });

    this.ws.send(JSON.stringify(body));
    return response;
  }

  handleMessage(raw) {
    let data;
    try {
      data = JSON.parse(raw);
    } catch {
      return;
    }
    if (data?.event) {
      this.emit(String(data?.op || ''), data);
      return;
    }
    const requestId = data?.requestId;
    if (!requestId) {
      this.emit(String(data?.op || ''), data);
      return;
    }
    const slot = this.pending.get(requestId);
    if (!slot) {
      this.emit(String(data?.op || ''), data);
      return;
    }
    this.pending.delete(requestId);
    slot.resolve(data);
  }

  on(op, handler) {
    const key = String(op || '').trim();
    if (!key || typeof handler !== 'function') {
      return () => {};
    }
    if (!this.listeners.has(key)) {
      this.listeners.set(key, new Set());
    }
    this.listeners.get(key).add(handler);
    return () => {
      const bucket = this.listeners.get(key);
      if (!bucket) return;
      bucket.delete(handler);
      if (!bucket.size) {
        this.listeners.delete(key);
      }
    };
  }

  emit(op, payload) {
    const bucket = this.listeners.get(String(op || '').trim());
    if (!bucket?.size) return;
    for (const handler of [...bucket]) {
      try {
        handler(payload);
      } catch {}
    }
  }

  failPending(message) {
    const error = new Error(message);
    for (const slot of this.pending.values()) slot.reject(error);
    this.pending.clear();
  }

  close() {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}
