"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SocketConnection = void 0;
const eventemitter3_1 = __importDefault(require("eventemitter3"));
const fast_json_patch_1 = __importDefault(require("fast-json-patch"));
const socket_io_client_1 = require("socket.io-client");
const with_resolver_js_1 = require("./with-resolver.js");
const generateUUID = () => {
    if (globalThis.crypto && globalThis.crypto.randomUUID) {
        return globalThis.crypto.randomUUID();
    }
    // A simple fallback for environments without crypto.randomUUID
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
        const r = (Math.random() * 16) | 0;
        const v = c === 'x' ? r : (r & 0x3) | 0x8;
        return v.toString(16);
    });
};
class SocketConnection extends eventemitter3_1.default {
    config;
    #socket = null;
    #subscriptions = new Map();
    #connectionPromise = null;
    constructor(config) {
        super();
        this.config = config;
    }
    #connect() {
        if (this.#socket?.connected) {
            return Promise.resolve();
        }
        if (this.#connectionPromise) {
            return this.#connectionPromise;
        }
        this.#connectionPromise = new Promise((resolve, reject) => {
            try {
                const socketUrl = `${this.config.baseUrl.replace(/^http/, 'ws')}/bamboo/${this.config.baseId}`;
                this.#socket = (0, socket_io_client_1.io)(socketUrl, {
                    auth: {
                        token: this.config.apiKey,
                        'time-zone': this.config.timeZone || 'UTC',
                        'client-id': this.config.clientId,
                    },
                    query: {},
                });
                this.#socket.on('successful', () => {
                    this.#socket?.on('patch', this.#handlePatch.bind(this));
                    this.#socket?.on('query-response', (response) => {
                        this.emit(response.queryId, response);
                    });
                    this.#socket?.emit('subscribe', { clientId: this.config.clientId });
                    resolve();
                });
                this.#socket.on('disconnect', () => {
                    this.#socket = null;
                    this.#connectionPromise = null;
                });
                this.#socket.on('connect_error', err => {
                    this.#socket = null;
                    this.#connectionPromise = null;
                    reject(err);
                });
            }
            catch (error) {
                this.#connectionPromise = null;
                reject(error);
            }
        });
        return this.#connectionPromise;
    }
    async rawRequest(query, variables, headers, transactionId) {
        await this.#connect();
        const queryId = generateUUID();
        const { promise, resolve } = (0, with_resolver_js_1.withResolver)();
        this.once(queryId, (response) => {
            if (response.errors) {
                resolve({
                    errors: response.errors,
                });
                return;
            }
            const firstKey = Object.keys(response.data)[0];
            if (firstKey) {
                resolve({ data: response.data[firstKey] });
            }
            else {
                resolve({ data: response.data });
            }
        });
        if (!headers) {
            headers = {};
        }
        headers = {
            ...headers,
            schema: 'readable',
        };
        this.emit('query', {
            query,
            variables,
            queryId,
            ...(transactionId ? { transactionId } : {}),
            ...(headers ? { options: { ...headers } } : {}),
        });
        const response = await promise;
        if ('errors' in response) {
            throw new Error(JSON.stringify(response.errors));
        }
        return response.data;
    }
    async subscribe(metadatas, callback) {
        await this.#connect();
        const param = metadatas.map(metadata => ({
            metadata,
            options: {
                returns: ['patch', 'affectedFields'],
            },
        }));
        const result = await this.rawRequest(`
      query ($param: [SubscriptionMetadataInput]!) {
        plugins {
          subscriptions {
            subscribe(subscriptions: $param) {
              subscriptionId
              data
            }
          }
        }
      }
      `, { param }, { 'client-id': this.config.clientId });
        const subscriptions = result.subscriptions.subscribe;
        const initialData = subscriptions.map(s => s.data);
        for (const sub of subscriptions) {
            this.#subscriptions.set(sub.subscriptionId, {
                data: sub.data,
                callback: (patchedData) => {
                    // This needs to be more robust for batching.
                    // For now, we'll just update the single data and call back.
                    const idx = subscriptions.findIndex(s => s.subscriptionId === sub.subscriptionId);
                    if (idx !== -1) {
                        initialData[idx] = patchedData;
                        callback(...initialData);
                    }
                },
            });
        }
        callback(...initialData);
        return {
            unsubscribe: async () => {
                const subscriptionIds = subscriptions.map(s => s.subscriptionId);
                for (const id of subscriptionIds) {
                    this.#subscriptions.delete(id);
                }
                await this.rawRequest(`query RemoveSubscription ($subscriptionIds: [String]) {
            plugins {
              subscriptions {
                unsubscribe(subscriptionIds: $subscriptionIds)
              }
            }
          }`, { subscriptionIds });
            },
        };
    }
    emit(event, ...args) {
        if (event === 'query') {
            this.#socket?.emit('query', ...args);
            return true;
        }
        return super.emit(event, ...args);
    }
    #handlePatch(response) {
        for (const patch of response.patches) {
            const sub = this.#subscriptions.get(patch.subscriptionId);
            if (sub) {
                // @ts-ignore
                const { newDocument } = fast_json_patch_1.default.applyPatch(sub.data, patch.delta);
                sub.data = newDocument;
                sub.callback(newDocument);
            }
        }
    }
}
exports.SocketConnection = SocketConnection;
//# sourceMappingURL=socket-connection.js.map