'use strict';

let iot = function () {

    let __currentTopics = new Set();
    let visibilityChangeHandler = null;
    let keepAliveIntervalId = null;
    let shouldBeConnected = false;
    let isConnecting = false;
    let interruptCount = 0;
    let subscriptionFailureCount = 0;

    return {

        __promise: null,

        __createNewWorker: () => {
            this.__privates.iot.log('Creating new worker...');
            this.__privates.iot.terminateWorker();
            this.__privates.iot.initWorker();
        },

        __updateCredentials: () => {

            if(isConnecting) {
                this.__privates.iot.log('Already reconnecting, ignoring update credentials');
                return Promise.resolve(this.iot.__promise);
            }

            this.__privates.iot.log('Updating Credentials...');
            interruptCount = 0;
            subscriptionFailureCount = 0;
            isConnecting = true;

            this.iot.__promise = this.iot.getCredentials()
                .then(response => this.__privates.iot.updateWebSocketCredentials(
                    response.data.credentials.accessKeyId,
                    response.data.credentials.secretAccessKey,
                    response.data.credentials.sessionToken,
                    response.data.credentials.expiration
                ))
                .then((r) => {
                    isConnecting = false;
                    this.__privates.iot.log('Credentials updated');
                    return Promise.resolve(r);
                })
                .catch(error => {
                    isConnecting = false;
                    this.__privates.iot.log('Failed to update credentials:', error);
                    return Promise.reject(error);
                });

            return this.iot.__promise;
        },

        getCredentials: () => {
            return this.__privates.auth.__client
                .then(client => client.apis.auth.iotSignIn({}, {requestBody: {suggestedTopics:true, domain: location.hostname}}))
        },

        onConnect: (data, connectionId) => {
            this.__privates.iot.log('MQTT client connected');
        },
        onClosed: (data, connectionId) => {
            this.__privates.iot.log('MQTT client closed');
            if(shouldBeConnected && !isConnecting) {
                this.__privates.iot.log('Connection unexpectedly closed, reconnecting...');
                this.iot.__createNewWorker();
                this.iot.__updateCredentials();
            }
        },
        onError: (data, connectionId) => {
            this.__privates.iot.log('MQTT client error');
        },
        onDisconnect: (data, connectionId) => {
            this.__privates.iot.log('MQTT client disconnect');
        },
        onInterrupt: (data, connectionId) => {
            if(shouldBeConnected && !isConnecting && interruptCount > 10) {
                this.__privates.iot.log('Interrupt count exceeded, reconnecting...');
                this.iot.__createNewWorker();
                this.iot.__updateCredentials();
            }
            interruptCount++;
            this.__privates.iot.log('MQTT client interrupt');
        },
        onResume: (data, connectionId) => {
            this.__privates.iot.log('MQTT client resume');
        },
        onConnectionSuccess: (data, connectionId) => {
            this.__privates.iot.log('MQTT client connection_success');
        },
        onConnectionFailure: (data, connectionId) => {
            if(shouldBeConnected && !isConnecting && interruptCount > 10) {
                this.__privates.iot.log('Interrupt count exceeded, reconnecting...');
                this.iot.__createNewWorker();
                this.iot.__updateCredentials();
            }
            interruptCount++;
            this.__privates.iot.log('MQTT client connection_failure');
        },

        iotLogin: (subscribeToSuggestedTopics = true) => {

            if(isConnecting) {
                this.__privates.iot.log('Already reconnecting, ignoring login');
                return this.iot.__promise;
            }

            interruptCount = 0;
            subscriptionFailureCount = 0;
            shouldBeConnected = true;
            isConnecting = true;

            this.iot.__createNewWorker();
            this.iot.__promise = new Promise((resolve, reject) => {
                this.iot.getCredentials()
                    .then(response => {
                        return Promise.all([
                            response.data.suggestedTopics,
                            this.__privates.iot.connect(
                                response.data.endpoint,
                                response.data.clientId,
                                response.data.region,
                                response.data.credentials.accessKeyId,
                                response.data.credentials.secretAccessKey,
                                response.data.credentials.sessionToken,
                                response.data.credentials.expiration,
                            )
                        ])
                    })
                    .then((r) => {
                        isConnecting = false;
                        resolve(r);
                    })
                    .catch((e) => {
                        isConnecting = false;
                        reject(e);
                    })
            });

            __currentTopics.clear();

            this.iot.$on('connect', this.iot.onConnect, this);
            this.iot.$on('closed', this.iot.onClosed, this);
            this.iot.$on('error', this.iot.onError, this);
            this.iot.$on('disconnect', this.iot.onDisconnect, this);
            this.iot.$on('interrupt', this.iot.onInterrupt, this);
            this.iot.$on('resume', this.iot.onResume, this);
            this.iot.$on('connection_success', this.iot.onConnectionSuccess, this);
            this.iot.$on('connection_failure', this.iot.onConnectionFailure, this);
            this.iot.$on('updateCredentials', this.iot.__updateCredentials, this);

            this.iot.__promise
                .then(([suggestedTopic, instance]) => {
                    if(!subscribeToSuggestedTopics) {
                        return  Promise.resolve(instance)
                    }
                    let topics = new Set([...suggestedTopic]); // Filter out any undefined or null topics
                    return Promise.all(Array.from(topics).map(topic => this.iot.subscribe(topic))).then(() => instance)
                })
                .then((instance) => {
                    this.iot.setupVisibilityChangeListener();
                    this.iot.setupKeepAliveInterval();
                    return instance;
                })
                .catch((error) => {
                    this.__privates.iot.log('MQTT Login Error:', error);
                    throw error;
                });

            return this.iot.__promise;
        },

        iotLogout: (keepCurrentTopics = false) => {

            if(!keepCurrentTopics) {
                __currentTopics.clear();
                this.__privates.iot.clearTopics();
            }

            interruptCount = 0;
            subscriptionFailureCount = 0;
            shouldBeConnected = false;

            this.iot.disableVisibilityChangeListener();
            this.iot.disableKeepAliveInterval();

            return this.__privates.iot.disconnect()
                .finally(() => {
                    this.__privates.iot.terminateWorker();
                    this.iot.$off('connect', this.iot.onConnect, this);
                    this.iot.$off('closed', this.iot.onClosed, this);
                    this.iot.$off('error', this.iot.onError, this);
                    this.iot.$off('disconnect', this.iot.onDisconnect, this);
                    this.iot.$off('interrupt', this.iot.onInterrupt, this);
                    this.iot.$off('resume', this.iot.onResume, this);
                    this.iot.$off('connection_success', this.iot.onConnectionSuccess, this);
                    this.iot.$off('connection_failure', this.iot.onConnectionFailure, this);
                    this.iot.$off('updateCredentials', this.iot.__updateCredentials, this);
                    this.iot.__promise = null;
                    return true;
                });
        },

        isConnected: () => {
            return this.__privates.iot.isConnected();
        },

        subscribe: (topic) => {
            if(!__currentTopics.has(topic)) {
                __currentTopics.add(topic);
                if(!this.iot.__promise) return Promise.reject('not_connected');
                return this.iot.__promise
                    .then(() => this.__privates.iot.subscribe(topic))
                    .catch((error) => {

                        // if subscription fails, remove the topic from the current topics
                        __currentTopics.delete(topic);

                        if(error?.cause === -1) {
                            return Promise.reject('invalid_topic or worker not initialized');
                        }

                        if(!shouldBeConnected || isConnecting) {
                            return Promise.reject('subscription_failed');
                        }

                        if(shouldBeConnected && !isConnecting && subscriptionFailureCount > 5) {
                            this.__privates.iot.log('Subscription failure exceeded, reconnecting...');
                            this.iot.__createNewWorker();
                            this.iot.__updateCredentials();
                            return Promise.reject('subscription_failed');
                        }

                        subscriptionFailureCount++;
                        return this.iot.subscribe(topic)
                    });
            }
        },

        unsubscribe: (topic) => {
            __currentTopics.delete(topic);
            if(!this.iot.__promise) return Promise.reject('not_connected');
            return this.iot.__promise
                .then(() => this.__privates.iot.unsubscribe(topic))
                .catch(() => {
                    // if unsubscription fails add the topic back to the current topics
                    __currentTopics.add(topic);
                    return Promise.reject('unsubscription_failed');
                });
        },

        send: (topic, message) => {
            return this.iot.__promise.then(() => this.__privates.iot.send(topic,message));
        },

        emitMessage: (message) => {
            this.__privates.iot.emitMessage(message);
        },

        $once: (key, callback, that) => {
            return this.__privates.iot.once(key, callback, that || this);
        },

        $on: (key, callback, that) => {
            return this.__privates.iot.on(key, callback, that || this);
        },

        $off: (key, callback, that) => {
            return this.__privates.iot.off(key, callback, that || this);
        },
        $clear: () => {
            return this.__privates.iot.clear();
        },
        $emit: (key, ...args) => {
            return this.__privates.iot.emit(key, ...args);
        },

        checkConnection: () => {
            return this.__privates.iot.checkConnection()
                .catch(error => {

                    if(error?.cause === -1) {
                        this.__privates.iot.log("We don't have a topic to check connection with");
                        return;
                    }

                    if(!shouldBeConnected) {
                        this.__privates.iot.log('Ignoring connection check failure, not connected');
                        return Promise.resolve();
                    }

                    if(isConnecting) {
                        this.__privates.iot.log('Connection check failed, but we are already reconnecting...');
                        return Promise.resolve();
                    }

                    this.__privates.iot.log('Connection check failed:', error);
                    this.iot.__createNewWorker();
                    return this.iot.__updateCredentials();
                });
        },

        setupKeepAliveInterval: () => {

            this.__privates.iot.log('Setting up keepalive interval');

            clearInterval(keepAliveIntervalId);
            keepAliveIntervalId = setInterval(() => {
                this.__privates.iot.checkConnection()
                    .catch(error => {

                        if(error?.cause === -1) {
                            this.__privates.iot.log("We don't have a topic to check connection with or check was aborted");
                            return;
                        }

                        if(shouldBeConnected && !isConnecting) {
                            this.__privates.iot.log('Keepalive failed:', error);
                            this.iot.__createNewWorker();
                            this.iot.__updateCredentials();
                        }
                    });
            }, 15000);
        },

        disableKeepAliveInterval: () => {
            clearInterval(keepAliveIntervalId);
            keepAliveIntervalId = null;
        },

        setupVisibilityChangeListener: () => {
            if (visibilityChangeHandler) {
                // Listener is already set up
                return;
            }
            visibilityChangeHandler = () => {
                if (!document.hidden) {
                    this.iot.checkConnection();
                }
            };
            document.addEventListener('visibilitychange', visibilityChangeHandler);
        },

        disableVisibilityChangeListener: () => {
            if (visibilityChangeHandler) {
                document.removeEventListener('visibilitychange', visibilityChangeHandler);
                visibilityChangeHandler = null;
            }
        }
    };
};

export default iot;
