/**
 * CollabClient — headless WebSocket client for the collaboration signaling server.
 *
 * Lives in app/ (no TUI dependency). Manages connection, room lifecycle, and
 * message dispatch. Emits events via registered callbacks.
 */
// ── Client ──────────────────────────────────────────────────────────────
export class CollabClient {
    ws = null;
    serverUrl;
    callbacks = {};
    _state = {
        roomId: null,
        peerId: null,
        peers: new Map(),
        connected: false
    };
    // Pending promise resolvers for request/response pairs.
    createResolver = null;
    joinResolver = null;
    rejectResolver = null;
    constructor(serverUrl) {
        this.serverUrl = serverUrl;
    }
    get state() {
        return this._state;
    }
    // ── Callback registration ───────────────────────────────────────────
    on(event, handler) {
        this.callbacks[event] = handler;
    }
    // ── Connection ──────────────────────────────────────────────────────
    connect() {
        if (this.ws && this._state.connected)
            return Promise.resolve();
        return new Promise((resolve, reject) => {
            try {
                this.ws = new WebSocket(this.serverUrl);
            }
            catch (err) {
                reject(new Error(`Failed to connect: ${err}`));
                return;
            }
            this.ws.addEventListener('open', () => {
                this._state.connected = true;
                resolve();
            });
            this.ws.addEventListener('error', (event) => {
                const msg = 'message' in event ? String(event.message) : 'WebSocket error';
                if (!this._state.connected) {
                    reject(new Error(msg));
                }
                else {
                    this.callbacks.onError?.(msg);
                }
            });
            this.ws.addEventListener('close', () => {
                const wasConnected = this._state.connected;
                this._state.connected = false;
                this.ws = null;
                if (wasConnected) {
                    this.callbacks.onDisconnect?.();
                }
            });
            this.ws.binaryType = 'arraybuffer';
            this.ws.addEventListener('message', (event) => {
                if (event.data instanceof ArrayBuffer) {
                    this.handleBinaryMessage(new Uint8Array(event.data));
                }
                else {
                    this.handleMessage(String(event.data));
                }
            });
        });
    }
    send(obj) {
        if (!this.ws || !this._state.connected)
            return;
        this.ws.send(JSON.stringify(obj));
    }
    // ── Binary frame helpers ─────────────────────────────────────────────
    // Format: [2-byte big-endian docId length][docId UTF-8 bytes][Yrs payload]
    handleBinaryMessage(data) {
        if (data.length < 2)
            return;
        const docIdLen = (data[0] << 8) | data[1];
        if (data.length < 2 + docIdLen)
            return;
        const docId = new TextDecoder().decode(data.subarray(2, 2 + docIdLen));
        const payload = data.subarray(2 + docIdLen);
        this.callbacks.onSyncData?.(docId, payload);
    }
    sendSyncData(docId, data) {
        if (!this.ws || !this._state.connected)
            return;
        const docIdBytes = new TextEncoder().encode(docId);
        const frame = new Uint8Array(2 + docIdBytes.length + data.length);
        frame[0] = (docIdBytes.length >> 8) & 0xff;
        frame[1] = docIdBytes.length & 0xff;
        frame.set(docIdBytes, 2);
        frame.set(data, 2 + docIdBytes.length);
        this.ws.send(frame);
    }
    // ── Message handler ─────────────────────────────────────────────────
    handleMessage(raw) {
        let msg;
        try {
            msg = JSON.parse(raw);
        }
        catch {
            return;
        }
        switch (msg.type) {
            case 'room_created':
                this.createResolver?.(msg.roomId);
                this.createResolver = null;
                this.rejectResolver = null;
                break;
            case 'joined':
                this._state.roomId = msg.roomId;
                this._state.peerId = msg.peerId;
                this._state.peers.clear();
                for (const p of msg.peers) {
                    this._state.peers.set(p.peerId, p);
                }
                this.joinResolver?.({
                    roomId: msg.roomId,
                    peerId: msg.peerId,
                    peers: msg.peers,
                    docIds: msg.docIds ?? []
                });
                this.joinResolver = null;
                this.rejectResolver = null;
                break;
            case 'peer_joined':
                this._state.peers.set(msg.peerId, {
                    peerId: msg.peerId,
                    displayName: msg.displayName
                });
                this.callbacks.onPeerJoined?.({ peerId: msg.peerId, displayName: msg.displayName });
                break;
            case 'peer_left': {
                const leavingPeer = this._state.peers.get(msg.peerId);
                this._state.peers.delete(msg.peerId);
                this.callbacks.onPeerLeft?.(msg.peerId, leavingPeer?.displayName);
                break;
            }
            case 'chat':
                this.callbacks.onChat?.(msg.peerId, msg.displayName, msg.text);
                break;
            case 'awareness': {
                const peer = this._state.peers.get(msg.peerId);
                if (peer) {
                    peer.sheets = msg.sheets;
                }
                this.callbacks.onAwareness?.(msg.peerId, msg.displayName, msg.sheets);
                break;
            }
            case 'error':
                this.callbacks.onError?.(msg.message);
                this.rejectResolver?.(new Error(msg.message));
                this.createResolver = null;
                this.joinResolver = null;
                this.rejectResolver = null;
                break;
        }
    }
    // ── Public API ──────────────────────────────────────────────────────
    async createRoom() {
        await this.connect();
        return new Promise((resolve, reject) => {
            this.createResolver = resolve;
            this.rejectResolver = reject;
            this.send({ type: 'create' });
        });
    }
    async joinRoom(roomId, displayName) {
        await this.connect();
        return new Promise((resolve, reject) => {
            this.joinResolver = resolve;
            this.rejectResolver = reject;
            this.send({ type: 'join', roomId, displayName });
        });
    }
    sendChat(text) {
        this.send({ type: 'chat', text });
    }
    sendAwareness(sheets) {
        this.send({ type: 'awareness', sheets });
    }
    leave() {
        this.send({ type: 'leave' });
        this._state.roomId = null;
        this._state.peerId = null;
        this._state.peers.clear();
    }
    destroy() {
        if (this.ws) {
            this.ws.close();
            this.ws = null;
        }
        this._state.connected = false;
        this._state.roomId = null;
        this._state.peerId = null;
        this._state.peers.clear();
    }
}
//# sourceMappingURL=collab-client.js.map