"use strict";
var RelayWebsocketClient_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RelayWebsocketClient = void 0;
const tslib_1 = require("tslib");
const codiac_common_1 = require("@codiac.io/codiac-common");
const inversify_1 = require("inversify");
const _ = require("lodash");
const WebSocket = require("ws");
const guid_typescript_1 = require("guid-typescript");
const stringify = require("json-stringify-safe");
const websocket_listener_settings_1 = require("./websocket-listener-settings");
const api_user_creds_1 = require("../../apis/codiac-api/common/api-user-creds");
/** Meta added to a message handler for facility in managing message subscribers. */
class HandlerSubx {
    constructor(id, handler) {
        this.id = id;
        this.handler = handler;
    }
}
let RelayWebsocketClient = RelayWebsocketClient_1 = class RelayWebsocketClient {
    static bootstrap(ioc, settings) {
        ioc.bind(websocket_listener_settings_1.WebsocketListenerSettings).toConstantValue(settings !== null && settings !== void 0 ? settings : {});
        ioc.bind(RelayWebsocketClient_1).toSelf().inSingletonScope(); // <-- There can be only one socket connection from the CLI.
    }
    constructor(credentials, settings, logger) {
        this.credentials = credentials;
        this.logger = logger;
        this.settings = _.assignIn({}, RelayWebsocketClient_1.DEFAULT_SETTINGS, settings);
        this.subscribers = {};
    }
    connect(connectionId) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                try {
                    this.socket = new WebSocket(this.settings.url, undefined, {
                        headers: {
                            "Authorization": this.credentials.apiToken
                        }
                    });
                    this.socket.onopen = (eventMsg) => {
                        this.logger.debug(`Connection opened successfully at [${eventMsg.target.url}]. Waiting on socket details message. `);
                    };
                    this.socket.onerror = (eventMsg) => {
                        var _a;
                        this.logger.error(`WebSocket ${eventMsg.type}: ${(_a = eventMsg.message) !== null && _a !== void 0 ? _a : "(no details available)"}`);
                        if (eventMsg.error) {
                            reject(eventMsg.error);
                        }
                        else {
                            reject(new Error(`WebSocket ${eventMsg.type} error: ${eventMsg.message}`));
                        }
                    };
                    this.subscribe("connection", (err, msg, meta, raw) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                        var _a;
                        this.logger.trace(`Connection msg received: `, msg);
                        this.connectionId = msg === null || msg === void 0 ? void 0 : msg.socketId;
                        resolve((_a = msg === null || msg === void 0 ? void 0 : msg.socketId) !== null && _a !== void 0 ? _a : RelayWebsocketClient_1.SOCKET_ID_UNDEFINED);
                    }));
                    this.socket.onmessage = (msg) => {
                        var _a, _b;
                        try {
                            if (msg.type === "message" && msg.target && msg.data) {
                                // MessageEvent entity
                                let contentsObj = JSON.parse(msg.data);
                                this.routeInboundMessage(contentsObj);
                            }
                            else {
                                this.routeInboundMessage(msg);
                            }
                        }
                        catch (err) {
                            this.logger.error(`Error routing message: ${(_a = err.name) !== null && _a !== void 0 ? _a : ""} ${(_b = err.message) !== null && _b !== void 0 ? _b : "(no dettails available)"}`);
                        }
                    };
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
    isConnected() {
        var _a;
        let isConnected = false;
        try {
            isConnected = Boolean(this.socket != undefined);
        }
        catch (err) {
            this.logger.error("Error checking connection: ".concat((_a = err.message) !== null && _a !== void 0 ? _a : ""), err.stack);
        }
        finally {
            return isConnected;
        }
    }
    get connectionId() {
        return this.cnxnId_backer;
    }
    set connectionId(value) {
        this.cnxnId_backer = value;
        return;
    }
    disconnect() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                var _a;
                try {
                    if (this.socket) {
                        let closingStates = [this.socket.CLOSED, this.socket.CLOSING];
                        if (!closingStates.includes(this.socket.readyState)) {
                            this.socket.close(1000); // for codes see: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
                            this.logger.debug(`Closed web socket.`);
                        }
                        else {
                            this.logger.debug(`Attempted close when socket was already closed or closing.`);
                        }
                    }
                    else {
                        this.logger.debug(`Attempted close when socket was undefined.`);
                    }
                    this.connectionId = undefined;
                    resolve();
                }
                catch (err) {
                    this.logger.error(`Error closing socket connection: ${(_a = err.message) !== null && _a !== void 0 ? _a : "(no details available)"}`);
                    reject(err);
                }
            }));
        });
    }
    // ---------------------------------------------------------------------------------------
    // PUB-SUB  (...well, sub, really)
    // ---------------------------------------------------------------------------------------
    publish(msg, meta) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            // TODO: Lift this from the ServerSideWebSocketClient
            throw new Error("publish is not yet implemented");
        });
    }
    /** Invoke and wait for a direct response (Request-response pattern). */
    request(msg, meta, timeoutInSecs) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            // TODO: Lift this from the ServerSideWebSocketClient
            throw new Error("request is not yet implemented");
        });
    }
    /** Returns the subscription id for the handler (reqd for unsubcribing).
     */
    subscribe(topics, handler) {
        if (this.subscribers[topics] == undefined) {
            this.subscribers[topics] = [];
        }
        let subx = {
            id: guid_typescript_1.Guid.create().toString(),
            handler: handler
        };
        this.subscribers[topics].push(subx);
        return subx.id;
    }
    /** Removes the given listener.  (IDEMPOTENT) Returns whether a handler was indeed removed (false if it didn't exist).
     *
     * Throws any exceptions. */
    // public async unsubscribe(action: string, subscriptionId: string): Promise<boolean> {
    unsubscribe(topics, options) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let existings = this.subscribers[topics];
            if (existings && existings.length > 0) {
                let removed;
                if (options === null || options === void 0 ? void 0 : options.subscriptionId) {
                    removed = _.remove(existings, x => x.id === options.subscriptionId);
                }
                else {
                    removed = this.subscribers[topics];
                    delete this.subscribers[topics];
                }
                return Boolean(removed.length > 0);
            }
            else {
                return false;
            }
        });
    }
    /** Fires any registered handlers.
     * (and doesnt wait for them).
    */
    routeInboundMessage(msg) {
        var _a, _b;
        if ((msg === null || msg === void 0 ? void 0 : msg.action) != undefined /* && msg.payload != undefined */) {
            let actionMsg = msg;
            let meta = this.extractInboundMeta(msg);
            let subxs = this.subscribers[meta.action];
            if (subxs && subxs.length > 0) {
                for (let subx of subxs) {
                    try {
                        this.logger.debug(`Handling inbound [${actionMsg.action}] SocketAction [handler id: ${subx.id}]...`);
                        // -----------------------------------------------------
                        //  !!!!  ATTENTION !!!!
                        // We're firing off all these handlers simulataneously!  
                        // TODO: consider await instead of async catch 
                        // (for sequential executions)  
                        subx.handler(null, msg.payload /* <-- HACK to allow for existing msgs wo payloads */, meta, msg)
                            .catch(reason => {
                            this.logger.error(`Unhandled error in [${actionMsg.action}] SocketAction handler [id: ${subx.id}]: ${reason !== null && reason !== void 0 ? reason : "(No details available)"}`);
                        });
                        // -----------------------------------------------------
                    }
                    catch (err) {
                        this.logger.error(`Error triggering [${actionMsg.action}] SocketAction handler [id: ${subx.id}]: ${(_a = err.name) !== null && _a !== void 0 ? _a : ""} ${(_b = err.message) !== null && _b !== void 0 ? _b : "(No details available)"}`);
                    }
                }
            }
            else {
                this.logger.warn(`Received unhandled [${actionMsg.action}] message from [${this.settings.url}];`);
            }
        }
        else {
            this.logger.warn(`Received unrecognized WebSocket message.`, stringify(msg));
        }
        return;
    }
    extractInboundMeta(msg) {
        var _a, _b, _c;
        try {
            let meta = {
                sid: (_b = (_a = msg.payload) === null || _a === void 0 ? void 0 : _a.socketId) !== null && _b !== void 0 ? _b : RelayWebsocketClient_1.SOCKET_ID_UNDEFINED,
                action: msg.action,
                subject: RelayWebsocketClient_1.NOT_USED_PLACEHOLDER,
                correlationId: msg.correlationId,
            };
            return meta;
        }
        catch (err) {
            this.logger.error("Error extracting meta from SocketAction: ".concat((_c = err.message) !== null && _c !== void 0 ? _c : "no details available"), err.stack);
            throw err;
        }
    }
};
RelayWebsocketClient.DEFAULT_SETTINGS = {
    url: "ws://localhost:5798",
    reconnectIntervalMs: 2000,
    /** Deliberately kept short because the retry is limited to a single cli command execution,
     * not an ongoing user session (thus it needs to fail fast).  */
    reconnectAttempts: 5
};
RelayWebsocketClient.SOCKET_ID_UNDEFINED = "undefined-socket-id";
RelayWebsocketClient.NOT_USED_PLACEHOLDER = "not-used";
RelayWebsocketClient = RelayWebsocketClient_1 = tslib_1.__decorate([
    (0, inversify_1.injectable)(),
    tslib_1.__param(0, (0, inversify_1.inject)(api_user_creds_1.ApiUserCreds)),
    tslib_1.__param(1, (0, inversify_1.inject)(websocket_listener_settings_1.WebsocketListenerSettings)),
    tslib_1.__param(2, (0, inversify_1.inject)(codiac_common_1.INTERFACES.IBetterLogger)),
    tslib_1.__metadata("design:paramtypes", [api_user_creds_1.ApiUserCreds,
        websocket_listener_settings_1.WebsocketListenerSettings, Object])
], RelayWebsocketClient);
exports.RelayWebsocketClient = RelayWebsocketClient;
//# sourceMappingURL=relay-websocket-client.js.map