"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebSocketWrapper = void 0;
exports.subscribeToEntityEvents = subscribeToEntityEvents;
const WebSocketNode = require("ws");
const User_1 = require("../../icc-api/model/User");
const HealthcareParty_1 = require("../../icc-api/model/HealthcareParty");
const Device_1 = require("../../icc-api/model/Device");
const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
function subscribeToEntityEvents(basePath, authApi, entityClass, eventTypes, filter, eventFired, options = {}, decryptor) {
    var _a, _b;
    const config = {
        User: {
            qualifiedName: 'org.taktik.icure.entities.User',
            decryptor: (data) => Promise.resolve(new User_1.User(Object.assign({}, data))),
        },
        Patient: {
            qualifiedName: 'org.taktik.icure.entities.Patient',
            decryptor: (data) => decryptor(data),
        },
        Service: {
            qualifiedName: 'org.taktik.icure.entities.embed.Service',
            decryptor: (data) => decryptor(data),
        },
        HealthElement: {
            qualifiedName: 'org.taktik.icure.entities.HealthElement',
            decryptor: (data) => decryptor(data),
        },
        Message: {
            qualifiedName: 'org.taktik.icure.entities.Message',
            decryptor: (data) => decryptor(data),
        },
        Topic: {
            qualifiedName: 'org.taktik.icure.entities.Topic',
            decryptor: (data) => decryptor(data),
        },
        MaintenanceTask: {
            qualifiedName: 'org.taktik.icure.entities.MaintenanceTask',
            decryptor: (data) => decryptor(data),
        },
        HealthcareParty: {
            qualifiedName: 'org.taktik.icure.entities.HealthcareParty',
            decryptor: (data) => Promise.resolve(new HealthcareParty_1.HealthcareParty(Object.assign({}, data))),
        },
        Device: {
            qualifiedName: 'org.taktik.icure.entities.Device',
            decryptor: (data) => Promise.resolve(new Device_1.Device(Object.assign({}, data))),
        },
        Contact: {
            qualifiedName: 'org.taktik.icure.entities.Contact',
            decryptor: (data) => decryptor(data),
        },
    };
    return WebSocketWrapper.create(basePath.replace('http', 'ws').replace('rest', 'ws') + '/notification/subscribe', new WebSocketAuthProviderImpl(authApi), (_a = options.connectionMaxRetry) !== null && _a !== void 0 ? _a : 5, (_b = options.connectionRetryIntervalMs) !== null && _b !== void 0 ? _b : 1000, {
        CONNECTED: [
            (ws) => __awaiter(this, void 0, void 0, function* () {
                const subscription = {
                    eventTypes,
                    entityClass: config[entityClass].qualifiedName,
                    filter: { filter },
                };
                ws.send(JSON.stringify(subscription));
            }),
        ],
    }, (data) => __awaiter(this, void 0, void 0, function* () {
        try {
            yield config[entityClass].decryptor(data).then((o) => eventFired(o));
        }
        catch (e) {
            console.error(e);
        }
    }), options.debug);
}
class WebSocketWrapper {
    constructor(url, authProvider, maxRetries = 3, retryDelay = 1000, statusCallbacks = {}, messageCallback = () => { }, debug = false) {
        this.url = url;
        this.authProvider = authProvider;
        this.maxRetries = maxRetries;
        this.retryDelay = retryDelay;
        this.statusCallbacks = statusCallbacks;
        this.messageCallback = messageCallback;
        this.debug = debug;
        this.pingLifetime = 20000;
        this.socket = null;
        this.retries = 0;
        this.closed = false;
        this.lastPingReceived = Date.now();
        this.intervalIds = [];
        this.methodPath = new URL(url).pathname;
    }
    static create(url, authProvider, maxRetries, retryDelay, statusCallbacks, messageCallback, debug) {
        return __awaiter(this, void 0, void 0, function* () {
            const ws = new WebSocketWrapper(url, authProvider, maxRetries, retryDelay, statusCallbacks, messageCallback, debug);
            yield ws.connect();
            return ws;
        });
    }
    send(data) {
        if (this.socket && this.socket.readyState === ReadyState.OPEN) {
            this.socket.send(data);
        }
    }
    close() {
        if (this.socket) {
            this.closed = true;
            this.socket.close(1000, 'Client closed connection');
        }
    }
    addStatusCallback(status, callback) {
        var _a, _b, _c, _d;
        switch (status) {
            case 'CONNECTED':
            case 'CLOSED':
                if (!this.statusCallbacks[status])
                    this.statusCallbacks[status] = [];
                (_b = (_a = this.statusCallbacks) === null || _a === void 0 ? void 0 : _a[status]) === null || _b === void 0 ? void 0 : _b.push(callback);
                break;
            case 'ERROR':
                (_d = (_c = this.statusCallbacks) === null || _c === void 0 ? void 0 : _c.ERROR) === null || _d === void 0 ? void 0 : _d.push(callback);
                break;
        }
    }
    connect() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.retries >= this.maxRetries) {
                throw new Error('WebSocket connection failed after ' + this.maxRetries + ' retries');
            }
            const bearerToken = yield this.authProvider.getBearerToken();
            if (!bearerToken) {
                throw new Error('Cannot obtain JWT for the user');
            }
            const socket = isNode
                ? new WebSocketNode(this.url, {
                    headers: {
                        Authorization: bearerToken,
                    },
                })
                : new WebSocket(`${this.url}?jwt=${bearerToken.slice(7) /* "Bearer ".length */}`);
            this.socket = new WebsocketAdapter(socket);
            this.socket.on('open', () => __awaiter(this, void 0, void 0, function* () {
                if (this.debug)
                    console.debug('WebSocket connection opened');
                this.intervalIds.push(setTimeout(() => {
                    this.retries = 0;
                }, (this.maxRetries + 1) * this.retryDelay));
                this.callStatusCallbacks('CONNECTED');
            }));
            this.socket.on('message', (event) => {
                if (this.debug)
                    console.debug('WebSocket message received', event);
                // Handle ping messages
                if (event === 'ping') {
                    if (this.debug)
                        console.debug('Received ping, sending pong');
                    this.send('pong');
                    this.lastPingReceived = Date.now();
                    this.intervalIds.push(setTimeout(() => {
                        var _a;
                        if (Date.now() - this.lastPingReceived > this.pingLifetime) {
                            console.error(`No ping received in the last ${this.pingLifetime} ms`);
                            (_a = this.socket) === null || _a === void 0 ? void 0 : _a.close();
                        }
                    }, this.pingLifetime));
                    return;
                }
                // Call the message callback for other messages
                try {
                    const data = JSON.parse(event);
                    this.messageCallback(data);
                }
                catch (error) {
                    console.error('Failed to parse WebSocket message', error);
                }
            });
            this.socket.on('close', ({ code, reason }) => {
                if (this.debug)
                    console.debug('WebSocket connection closed', code, reason === null || reason === void 0 ? void 0 : reason.toString());
                this.callStatusCallbacks('CLOSED');
                this.intervalIds.forEach((id) => clearTimeout(id));
                this.intervalIds = [];
                if (this.closed) {
                    return;
                }
                setTimeout(() => __awaiter(this, void 0, void 0, function* () {
                    ++this.retries;
                    return yield this.connect();
                }), this.retryDelay);
            });
            this.socket.on('error', (err) => __awaiter(this, void 0, void 0, function* () {
                console.error('WebSocket error', err);
                this.callStatusCallbacks('ERROR', err);
                if (this.socket) {
                    this.socket.close();
                }
            }));
        });
    }
    callStatusCallbacks(event, error) {
        var _a, _b, _c, _d;
        switch (event) {
            case 'CONNECTED':
            case 'CLOSED':
                (_b = (_a = this.statusCallbacks) === null || _a === void 0 ? void 0 : _a[event]) === null || _b === void 0 ? void 0 : _b.forEach((callback) => callback(this));
                break;
            case 'ERROR':
                (_d = (_c = this.statusCallbacks) === null || _c === void 0 ? void 0 : _c.ERROR) === null || _d === void 0 ? void 0 : _d.forEach((callback) => callback(this, error));
                break;
        }
    }
}
exports.WebSocketWrapper = WebSocketWrapper;
class WebSocketAuthProviderImpl {
    constructor(authApi) {
        this.authApi = authApi;
    }
    getBearerToken() {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const headers = yield this.authApi.authenticationProvider.getAuthService().getAuthHeaders();
            return (_a = headers.find((header) => header.header === 'Authorization' && header.data.startsWith('Bearer '))) === null || _a === void 0 ? void 0 : _a.data;
        });
    }
}
var ReadyState;
(function (ReadyState) {
    ReadyState[ReadyState["CONNECTING"] = 0] = "CONNECTING";
    ReadyState[ReadyState["OPEN"] = 1] = "OPEN";
    ReadyState[ReadyState["CLOSING"] = 2] = "CLOSING";
    ReadyState[ReadyState["CLOSED"] = 3] = "CLOSED";
})(ReadyState || (ReadyState = {}));
class WebsocketAdapter {
    constructor(websocket) {
        this.websocket = websocket;
    }
    get readyState() {
        if (this.websocket) {
            if (isNode) {
                switch (this.websocket.readyState) {
                    case WebSocketNode.CONNECTING:
                        return ReadyState.CONNECTING;
                    case WebSocketNode.OPEN:
                        return ReadyState.OPEN;
                    case WebSocketNode.CLOSING:
                        return ReadyState.CLOSING;
                    case WebSocketNode.CLOSED:
                        return ReadyState.CLOSED;
                }
            }
            else {
                switch (this.websocket.readyState) {
                    case WebSocket.CONNECTING:
                        return ReadyState.CONNECTING;
                    case WebSocket.OPEN:
                        return ReadyState.OPEN;
                    case WebSocket.CLOSING:
                        return ReadyState.CLOSING;
                    case WebSocket.CLOSED:
                        return ReadyState.CLOSED;
                }
            }
        }
        return ReadyState.CLOSED;
    }
    /**
     * This is not throwing an error if the websocket is not open, because the purpose of the websocket wrapper is custom to notifications needs (subscribe and ping pong only)
     * If in the future we need to use it for other purposes, we will have to throw an error and update the implementation of ping pong in the notification
     * @param data the data to send
     */
    send(data) {
        if (this.websocket && this.readyState === ReadyState.OPEN) {
            this.websocket.send(data);
        }
    }
    close(code, reason) {
        if (this.websocket) {
            this.websocket.close(code, reason);
        }
    }
    onMessage(callback) {
        if (this.websocket) {
            if (isNode) {
                ;
                this.websocket.on('message', (x) => callback(x.toString('utf8')));
            }
            else {
                ;
                this.websocket.addEventListener('message', (event) => __awaiter(this, void 0, void 0, function* () {
                    let dataAsString;
                    if (event.type === 'message') {
                        dataAsString = event.data;
                    }
                    else
                        console.error('Unexpected event type: ' + event.type);
                    if (!dataAsString) {
                        console.error('Failed to parse WebSocket message');
                        return;
                    }
                    callback(dataAsString);
                }));
            }
        }
        else {
            console.error('Websocket is not defined');
            throw new Error('Websocket is not defined');
        }
    }
    onOpen(callback) {
        if (this.websocket) {
            if (isNode) {
                ;
                this.websocket.on('open', callback);
            }
            else {
                ;
                this.websocket.addEventListener('open', callback);
            }
        }
    }
    onClose(callback) {
        if (this.websocket) {
            if (isNode) {
                ;
                this.websocket.on('close', callback);
            }
            else {
                ;
                this.websocket.addEventListener('close', callback);
            }
        }
    }
    onError(callback) {
        if (this.websocket) {
            if (isNode) {
                ;
                this.websocket.on('error', callback);
            }
            else {
                ;
                this.websocket.addEventListener('error', callback);
            }
        }
    }
    on(event, callback) {
        if (this.websocket) {
            switch (event) {
                case 'message':
                    return this.onMessage(callback);
                case 'open':
                    return this.onOpen(callback);
                case 'close':
                    return this.onClose(callback);
                case 'error':
                    return this.onError(callback);
            }
        }
    }
}
//# sourceMappingURL=websocket.js.map