import WebSocket from 'isomorphic-ws';

/**
 * Maximum websocket frame size, in bytes, for sockets on both ends of the
 * editor <-> dev server connection. 100 MiB matches the `ws` library default;
 * it is set explicitly so the limit is a deliberate choice rather than an
 * inherited default. Inbound frames over the limit make `ws` close that
 * single socket with 1009 (Message Too Big).
 *
 * Only applies where `ws` is the implementation (Node). Browsers ignore the
 * option and don't enforce a receive limit.
 */
export const MAX_SOCKET_PAYLOAD_BYTES = 100 * 1024 * 1024;

/**
 * Default User-Agent for Node-originated WebSocket handshakes. The `ws` client
 * sends no User-Agent by default (unlike browsers, axios, or undici's fetch),
 * and AWS WAF's managed common rule set blocks UA-less requests
 * (`NoUserAgent_HEADER`) with a 403 at the ALB. A non-empty UA keeps the
 * handshake from being rejected; callers may override with a more specific,
 * versioned identity. Ignored by browsers (native WebSocket sets its own UA).
 */
export const DEFAULT_WEBSOCKET_USER_AGENT = 'superblocks-client';

/**
 * Build a client User-Agent of the form `component/version`, or bare `component`
 * when no version is supplied. Single source of truth for the identity strings
 * Node WebSocket clients (CLI, dev server, ...) send on the handshake.
 */
export function buildClientUserAgent(component: string, version?: string): string {
  return version ? `${component}/${version}` : component;
}

export interface ConnectWebSocketOptions {
  protocol?: string | string[];
  timeout?: number;
  /** Extra handshake headers (Node only; ignored by browsers). Overrides the default User-Agent. */
  headers?: Record<string, string>;
}

/**
 * If the connection is not established within the timeout, the promise will be rejected.
 * This can happen with bad network conditions, but is very rare.
 */
export function connectWebSocket(wsUrl: string, options: ConnectWebSocketOptions = {}): Promise<WebSocket> {
  const { protocol, timeout = 30_000, headers } = options;
  return new Promise((resolve, reject) => {
    const ws = new WebSocket(wsUrl, protocol, {
      maxPayload: MAX_SOCKET_PAYLOAD_BYTES,
      headers: { 'User-Agent': DEFAULT_WEBSOCKET_USER_AGENT, ...headers }
    });

    const timeoutId = setTimeout(() => {
      reject(new Error('[internal] WebSocket connection timeout'));
    }, timeout);

    ws.addEventListener('open', () => {
      clearTimeout(timeoutId);
      // Resolve the promise with the WebSocket instance when the connection is open
      resolve(ws);
    });

    ws.addEventListener('error', (event) => {
      clearTimeout(timeoutId);
      // Wrap browser WebSocket errors with [internal] tag so they can be filtered
      // from Clark's context (raw browser errors don't have useful stack traces)
      const message = 'message' in event && typeof event.message === 'string' ? event.message : 'WebSocket connection failed';
      reject(new Error(`[internal] ${message}`));
    });
  });
}
