import emitter from './wt-emitter';
import {decodeJanusDisplay, generateUUID} from "../models/utils";
import Worker from './wt-iot-worker.worker.js';

class Iot {
    constructor(enableDebugFlag) {
        Object.assign(this, emitter());
        this.decoder = new TextDecoder('utf-8');
        this.log = Iot.noop;
        this.credentialsExpirationCheckIntervalId = null;
        this.currentCredentialsExpirationStamp = null;
        this.lastConnectParams = null;
        this.subscribedTopics = new Set();
        this.abortController = null;
        this.worker = null;

        if (enableDebugFlag) {
            this.enableDebug();
        }
    }

    static noop() {}

    enableDebug() {
        this.log = console.log.bind(console);
    }

    initWorker() {
        this.log('iot initWorker');
        this.worker = new Worker();
        this.worker.onmessage = this.handleWorkerMessage.bind(this);
    }

    terminateWorker() {
        this.log('iot terminateWorker');
        this.worker?.terminate();
    }

    connect(apiMqttUrl, apiMqttClientId, region, accessKeyId, secretAccessKey, sessionToken, expiration) {
        this.log('iot connect called, we disconnect first just to be sure');
        return this.disconnect()
            .catch(() => {
                // we dont care if disconnect fails
                return Promise.resolve();
            })
            .then(() => {
                this.log('iot connect');

                if(!this.worker) {
                    this.log('Worker not initialized');
                    return Promise.reject(new Error('Worker not initialized', {cause: -1}));
                }

                this.abortController = new AbortController();
                this.startCredentialsExpirationCheck(expiration);
                this.lastConnectParams = { apiMqttUrl, apiMqttClientId, region, accessKeyId, secretAccessKey, sessionToken, expiration };
                return new Promise((resolve, reject) => {
                    const stamp = new Date().getTime() +'_'+ generateUUID();

                    const handleConnectResult = (event) => {

                        if(event.stamp !== stamp) {
                            this.log('connect event stamp mismatch, ignoring', event.stamp, stamp);
                            return;
                        }

                        clearTimeout(timeoutId);
                        this.off('worker:connect_result', handleConnectResult);
                        if (event.success) {
                            resolve();
                        } else {
                            reject(new Error(event.error));
                        }
                    };

                    const timeoutId = setTimeout(() => {
                        this.off('worker:connect_result', handleConnectResult);
                        reject(new Error('Connection timeout'));
                    }, 10000);

                    this.on('worker:connect_result', handleConnectResult);

                    this.worker?.postMessage({
                        type: 'connect',
                        params: this.lastConnectParams,
                        stamp: stamp
                    });
                });
            });
    }

    disconnect() {
        this.log('iot disconnect');
        this.stopCredentialsExpirationCheck();
        this.abortController?.abort();

        if(!this.worker) {
            this.log('Worker not initialized');
            return Promise.reject(new Error('Worker not initialized', {cause: -1}));
        }

        return new Promise((resolve, reject) => {

            const stamp = new Date().getTime()+'_'+ generateUUID();

            const handleDisconnectResult = (event) => {
                if(event.stamp !== stamp) {
                    this.log('disconnect event stamp mismatch, ignoring', event.stamp, stamp);
                    return;
                }
                clearTimeout(timeoutId);
                this.off('worker:disconnect_result', handleDisconnectResult);
                if (event.success) {
                    resolve();
                } else {
                    reject(new Error(event.error));
                }
            };

            const timeoutId = setTimeout(() => {
                this.off('worker:disconnect_result', handleDisconnectResult);
                reject(new Error('Disconnect timeout'));
            }, 5000);

            this.on('worker:disconnect_result', handleDisconnectResult);
            this.worker?.postMessage({ type: 'disconnect', stamp: stamp });
        });
    }

    isConnected() {

        if(!this.worker) {
            this.log('Worker not initialized');
            return Promise.reject(new Error('Worker not initialized', {cause: -1}));
        }

        return new Promise((resolve) => {
            const handleIsConnectedResult = (event) => {
                this.off('worker:is_connected_result', handleIsConnectedResult);
                resolve(event.connected);
            };

            this.on('worker:is_connected_result', handleIsConnectedResult);
            this.worker?.postMessage({ type: 'is_connected' });
        });
    }

    clearTopics() {
        this.subscribedTopics.clear();
        this.worker?.postMessage({ type: 'clear_topics' });
    }

    subscribe(topic) {
        this.log('iot subscribe', topic);
        if (typeof topic === 'string' && topic.trim() !== '') {

            if(!this.subscribedTopics.has(topic)) {
                this.subscribedTopics.add(topic);
            }

            if(!this.worker) {
                this.log('Worker not initialized');
                return Promise.reject(new Error('Worker not initialized', {cause: -1}));
            }

            return new Promise((resolve, reject) => {
                const stamp = new Date().getTime()+'_'+ generateUUID();
                const handleSubscribeResult = (event) => {

                    if(event.stamp !== stamp) {
                        this.log('subscribe event stamp mismatch, ignoring', event.stamp, stamp);
                        return;
                    }

                    this.off('worker:subscribe_result', handleSubscribeResult);
                    if (event.success) {
                        resolve();
                    } else {
                        reject(new Error(event.error));
                    }
                };

                this.on('worker:subscribe_result', handleSubscribeResult);
                this.worker?.postMessage({ type: 'subscribe', topic, stamp });
            });
        } else {
            this.log('Invalid topic:', topic);
            return Promise.reject(new Error('Invalid topic', {cause: -1}));
        }
    }

    unsubscribe(topic) {
        this.log('iot unsubscribe', topic);
        if (typeof topic === 'string' && topic.trim() !== '') {
            this.subscribedTopics.delete(topic);
            if(!this.worker) {
                this.log('Worker not initialized');
                return Promise.reject(new Error('Worker not initialized', {cause: -1}));
            }

            return new Promise((resolve, reject) => {

                const stamp = new Date().getTime()+'_'+ generateUUID();;

                const handleUnsubscribeResult = (event) => {

                    if(event.stamp !== stamp) {
                        this.log('unsubscribe event stamp mismatch, ignoring', event.stamp, stamp);
                        return;
                    }

                    this.off('worker:unsubscribe_result', handleUnsubscribeResult);
                    if (event.success) {
                        resolve();
                    } else {
                        reject(new Error(event.error));
                    }
                };

                this.on('worker:unsubscribe_result', handleUnsubscribeResult);
                this.worker?.postMessage({ type: 'unsubscribe', topic, stamp });
            });
        } else {
            this.log('Invalid topic:', topic);
            return Promise.reject(new Error('Invalid topic'));
        }
    }

    send(topic, message) {
        this.log('iot send', topic, message);
        let msg = typeof message === 'object' ? JSON.stringify(message) : message;
        this.worker?.postMessage({ type: 'send', topic, message: msg });
    }

    emitMessage(message) {
        this.emit('message', message)
    }

    handleWorkerMessage(event) {
        const { type, data, connectionId } = event.data;
        switch (type) {
            case 'message':
                this.handleMessage(data.topic, new Uint8Array(data.payload));
                break;
            case 'connect':
            case 'disconnect':
            case 'error':
            case 'interrupt':
            case 'resume':
            case 'connection_success':
            case 'connection_failure':
                this.emit(type, data, connectionId);
                break;
            case 'connect_result':
            case 'disconnect_result':
            case 'is_connected_result':
            case 'subscribe_result':
            case 'unsubscribe_result':
                this.emit(`worker:${type}`, data);
                break;
        }
    }

    handleMessage(topic, payload) {
        const topicParts = topic.split('/');
        let message;
        try {
            message = JSON.parse(this.decoder.decode(payload));
        } catch (error) {
            this.log('Error parsing message:', error);
            return;
        }

        if(message.display) {
            const decodedDisplay = decodeJanusDisplay(message.display);
            if(decodedDisplay.userId) {
                message = {...message, userId: decodedDisplay.userId, role: decodedDisplay.role, start: decodedDisplay.start};
            }
        }

        if(topicParts[0] === 'user') { // user
            const userId = topicParts[1].replace("_", ':');
            this.emit('message', {userId, ...message, event: message.event ? `user:${message.event}` : 'user'});
        } else if(topicParts[0] === 'wt') {
            const event = message.event;
            const roomId = topicParts[2];
            if(topicParts[1] === 'room') { // room
                if(
                    event === 'message' ||
                    event === 'template_updated' ||
                    event === 'record_start' ||
                    event === 'record_stop' ||
                    event === 'record_configured' ||
                    event === 'record_livestream_available' ||
                    event === 'record_livestream_kick' ||
                    event === 'user_update_displayname' ||
                    event === 'user_update_avatar' ||
                    event === 'user_update_bio' ||
                    event === 'user_update_customattributes' ||
                    event === 'user_update_privateattributes' ||
                    event === 'channel_changed' ||
                    event === "instance_homepage_changed" ||
                    event === "instance_settings_changed" ||
                    event === "externalmix_changed" ||
                    event === "video_uploaded" ||
                    event === "change_user_devices" ||
                    event === "queue" ||
                    event === "title_changed" ||
                    event === "videowall_changed" ||
                    event === 'left' || //user removed room a.k.a. left the room
                    event === 'kicked' ||
                    event === 'banned' ||
                    event === 'unbanned' ||
                    event === 'approved' ||
                    event === 'muted' ||
                    event === 'unmuted' ||
                    event === 'messageRemoved' ||
                    event === 'messageReported' ||
                    event === 'chatClear' ||
                    event === 'handRaised' ||
                    event === 'handLowered' ||
                    event === 'handsCleared' ||
                    event === 'volume_set' ||
                    event === 'asset_created' ||
                    event === 'attribute_updated'
                ) {
                    this.emit('message', {event, ...message, roomId})
                }
                else if(event === 'joined' || event === 'leaving') {
                    this.emit('message', {event, ...message, isObserver:!!message.isObserver, roomId});
                }
            }
            else if(topicParts[1] === 'instanceroom') { // instance
                if(event === 'add_room' || event === 'remove_room' || event === 'set_room' || event === "instance_homepage_changed" || event === 'instance_settings_changed') {
                    this.emit('message', {event, ...message});
                }
            }
            else if(topicParts[1] === 'externalmix') {
                const event = message.event;
                this.emit('message', {event, ...message});
            }
            else if(topicParts[1] === 'asset') {
                this.emit('message',  {event: 'asset', assetId: topicParts[2], ...message});
            }
        } else if(topicParts[0] === 'wtr' || topicParts[0] === 'gwtr') {
            const recorderId = topicParts[1];
            const sessionId = topicParts[2];
            if(topicParts[3] === 'control') {
                this.emit('message', {event: 'recorder_control', ...message, recorderId, sessionId});
            } // recorder control
            else if(topicParts[3] === 'monitor') {
                this.emit('message', {event: 'recorder_monitor', ...message, recorderId, sessionId});
            } // recorder monitor
        }
    }

    startCredentialsExpirationCheck(expiration) {
        this.stopCredentialsExpirationCheck();
        this.currentCredentialsExpirationStamp = new Date(expiration).getTime();
        this.credentialsExpirationCheckIntervalId = setInterval(() => {
            const currentTimeStamp = new Date().getTime();
            // update 15 minutes before expiration
            if(this.currentCredentialsExpirationStamp - currentTimeStamp <= 900000) {
                this.log('iot credentials expired, updating');
                this.emit('updateCredentials');
            }
        }, 5000);
    }

    stopCredentialsExpirationCheck() {
        clearInterval(this.credentialsExpirationCheckIntervalId);
        this.credentialsExpirationCheckIntervalId = null;
    }

    updateWebSocketCredentials(accessKeyId, secretAccessKey, sessionToken, expiration) {
        this.log('iot updateWebSocketCredentials');
        this.lastConnectParams = {...this.lastConnectParams, accessKeyId, secretAccessKey, sessionToken, expiration };
        const currentTopics = new Set(this.subscribedTopics);
        return this.connect(
            this.lastConnectParams.apiMqttUrl,
            this.lastConnectParams.apiMqttClientId,
            this.lastConnectParams.region,
            accessKeyId,
            secretAccessKey,
            sessionToken,
            expiration
        ).then(() => {
            // Resubscribe to topics
            currentTopics.forEach(topic => this.subscribe(topic));
            return true;
        });
    }

    checkConnection() {
        return new Promise((resolve, reject) => {
            if (this.subscribedTopics.size === 0) {
                reject(new Error('No subscribed topics available for connection check', {cause: -1}));
                return;
            }
            let suitableTopic = Array.from(this.subscribedTopics).find(topic => topic.indexOf('user') > -1);
            if (!suitableTopic) {
                reject(new Error('No suitable topic found for connection check', {cause: -1}));
                return;
            }
            const testMessage = {
                type: 'keep_alive',
                timestamp: Date.now()
            };
            const timeoutId = setTimeout(() => {
                this.off('message', checkMessageHandler);
                this.abortController.signal.removeEventListener('abort', abort);
                reject(new Error('Connection check timeout'));
            }, 5000);

            const abort = () => {
                clearTimeout(timeoutId);
                this.off('message', checkMessageHandler);
                this.abortController.signal.removeEventListener('abort', abort);
                reject(new Error('Connection check aborted', {cause: -1}));
            };

            this.abortController.signal.addEventListener('abort', abort);

            const checkMessageHandler = (message) => {
                if (message.type === 'keep_alive' && message.timestamp === testMessage.timestamp) {
                    clearTimeout(timeoutId);
                    this.abortController.signal.removeEventListener('abort', abort);
                    this.off('message', checkMessageHandler);
                    resolve();
                }
            };

            this.on('message', checkMessageHandler);

            this.send(suitableTopic, testMessage);
        });
    }
}

export default Iot;
