/**
 * WebSocket client for connecting to the gateway server.
 *
 * Handles connection lifecycle, message sending/receiving,
 * ping/pong heartbeat responses, and automatic reconnection
 * with exponential backoff.
 */
import WebSocket from "ws";
const DEFAULT_RECONNECT_BASE_MS = 1000;
const DEFAULT_RECONNECT_MAX_MS = 60000;
export class GatewayClient {
    ws = null;
    _state = "disconnected";
    listeners = new Map();
    url;
    autoReconnect;
    reconnectBaseMs;
    reconnectMaxMs;
    reconnectAttempt = 0;
    reconnectTimer = null;
    manualDisconnect = false;
    constructor(options) {
        this.url = options.url;
        this.autoReconnect = options.autoReconnect ?? false;
        this.reconnectBaseMs = options.reconnectBaseMs ?? DEFAULT_RECONNECT_BASE_MS;
        this.reconnectMaxMs = options.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
    }
    /** Current connection state */
    get state() {
        return this._state;
    }
    /** Register an event listener */
    on(event, listener) {
        let set = this.listeners.get(event);
        if (!set) {
            set = new Set();
            this.listeners.set(event, set);
        }
        set.add(listener);
        return this;
    }
    /** Remove an event listener */
    off(event, listener) {
        const set = this.listeners.get(event);
        if (set) {
            set.delete(listener);
        }
        return this;
    }
    /** Emit an event to all registered listeners */
    emit(event, ...args) {
        const set = this.listeners.get(event);
        if (set) {
            for (const listener of set) {
                try {
                    listener(...args);
                }
                catch {
                    // Don't let listener errors crash the client
                }
            }
        }
    }
    /** Connect to the gateway server */
    connect() {
        if (this._state === "connected" || this._state === "connecting") {
            return;
        }
        this.manualDisconnect = false;
        this._state = "connecting";
        this.setupWebSocket();
    }
    /** Set up a new WebSocket connection and its event handlers */
    setupWebSocket() {
        this.ws = new WebSocket(this.url);
        this.ws.on("open", () => {
            const wasReconnecting = this._state === "reconnecting";
            this._state = "connected";
            this.reconnectAttempt = 0;
            if (wasReconnecting) {
                this.emit("reconnected");
            }
            else {
                this.emit("connected");
            }
        });
        this.ws.on("message", (data) => {
            try {
                const parsed = JSON.parse(data.toString());
                // Auto-respond to ping with pong
                if (parsed.type === "ping") {
                    this.sendJson({ type: "pong" });
                    this.emit("message", parsed);
                    return;
                }
                this.emit("message", parsed);
            }
            catch {
                // Ignore unparseable messages
            }
        });
        this.ws.on("close", (code, reason) => {
            const prev = this._state;
            this.ws = null;
            if (prev === "disconnected" || this.manualDisconnect) {
                this._state = "disconnected";
                return;
            }
            this._state = "disconnected";
            this.emit("disconnected", code, reason.toString());
            if (this.autoReconnect) {
                this.scheduleReconnect();
            }
        });
        this.ws.on("error", (err) => {
            this.emit("error", err);
        });
    }
    /** Schedule a reconnection attempt with exponential backoff */
    scheduleReconnect() {
        this.reconnectAttempt++;
        const delay = Math.min(this.reconnectBaseMs * Math.pow(2, this.reconnectAttempt - 1), this.reconnectMaxMs);
        this._state = "reconnecting";
        this.emit("reconnecting", this.reconnectAttempt, delay);
        this.reconnectTimer = setTimeout(() => {
            this.reconnectTimer = null;
            if (!this.manualDisconnect) {
                this.setupWebSocket();
            }
        }, delay);
    }
    /** Send a typed message to the gateway */
    send(message) {
        this.sendJson(message);
    }
    /** Send any JSON-serializable object to the gateway */
    sendJson(data) {
        if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
            throw new Error("WebSocket is not connected");
        }
        this.ws.send(JSON.stringify(data));
    }
    /** Disconnect from the gateway server gracefully */
    disconnect() {
        this.manualDisconnect = true;
        this._state = "disconnected";
        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer);
            this.reconnectTimer = null;
        }
        if (this.ws) {
            this.ws.close(1000, "Client disconnect");
            this.ws = null;
        }
    }
    /** Check if the client is currently connected */
    get connected() {
        return this._state === "connected";
    }
    /**
     * Send a WebSocket ping and wait for pong response.
     * Uses WebSocket protocol-level ping/pong for true end-to-end verification.
     *
     * @param timeoutMs - Maximum time to wait for pong (default 3000ms)
     * @returns true if pong received within timeout, false otherwise
     */
    async ping(timeoutMs = 3000) {
        if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
            return false;
        }
        return new Promise((resolve) => {
            const timeout = setTimeout(() => {
                this.ws?.off("pong", onPong);
                resolve(false);
            }, timeoutMs);
            const onPong = () => {
                clearTimeout(timeout);
                resolve(true);
            };
            this.ws.once("pong", onPong);
            this.ws.ping();
        });
    }
}
//# sourceMappingURL=websocket-client.js.map