import emitter from './wt-emitter';
import { decodeJanusDisplay } from "../models/utils";
import { mqtt, iot } from 'aws-iot-device-sdk-v2';

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.connection = null;
        this.subscribedTopics = new Set();

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

    static noop() {}

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

    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');
                this.startCredentialsExpirationCheck(expiration);
                this.lastConnectParams = { apiMqttUrl, apiMqttClientId, region, accessKeyId, secretAccessKey, sessionToken, expiration };

                const configBuilder = iot.AwsIotMqttConnectionConfigBuilder.new_with_websockets();

                configBuilder.with_clean_session(true);
                configBuilder.with_client_id(apiMqttClientId);
                configBuilder.with_endpoint(apiMqttUrl);
                configBuilder.with_credentials(region, accessKeyId, secretAccessKey, sessionToken);
                configBuilder.with_keep_alive_seconds(30);
                configBuilder.with_ping_timeout_ms(3000);
                configBuilder.with_reconnect_max_sec(5);
                configBuilder.with_reconnect_min_sec(1);

                const config = configBuilder.build();

                const client = new mqtt.MqttClient();
                this.connection = client.new_connection(config);

                this.connection.on('connect', () => {
                    this.emit('connect');
                });

                this.connection.on('closed', (error) => {
                    this.emit('closed', error);
                });

                this.connection.on('disconnect', () => {
                    this.emit('disconnect');
                });

                this.connection.on('error', (error) => {
                    this.emit('error', error);
                });

                this.connection.on('interrupt', (error) => {
                    this.emit('interrupt', error);
                });

                this.connection.on('resume', (error) => {
                    this.emit('resume', error);
                });

                this.connection.on('message', (topic, payload) => {
                    this.handleMessage(topic, new Uint8Array(payload));
                });

                this.connection.on('connection_success', (error) => {
                    this.emit('connection_success', error);
                });

                this.connection.on('connection_failure', (error) => {
                    this.emit('connection_failure', error);
                });

                return new Promise((resolve, reject) => {
                    const timeoutId = setTimeout(() => {
                        reject(new Error('Connection timeout'));
                    }, 5000); // 5 seconds timeout is enough
                    this.connection.connect()
                        .then((r) => {
                            clearTimeout(timeoutId);
                            resolve(r);
                        })
                        .catch((error) => {
                            clearTimeout(timeoutId);
                            reject(error);
                        });
                });
            });
    }


    disconnect() {
        this.log('iot disconnect');
        this.stopCredentialsExpirationCheck();
        if (this.connection) {
            return new Promise((resolve, reject) => {
                const timeoutId = setTimeout(() => {
                    reject(new Error('Disconnect timeout'));
                }, 5000); // 5 seconds timeout is enough
                this.connection.disconnect()
                    .then((r) => {
                        clearTimeout(timeoutId);
                        resolve(r);
                    })
                    .catch((error) => {
                        clearTimeout(timeoutId);
                        reject(error);
                    });
            });
        } else return Promise.resolve();
    }

    isConnected() {
        return this.connection && this.connection.currentState === 0 && this.connection.desiredState === 0;
    }

    clearTopics() {
        this.subscribedTopics.clear();
    }

    subscribe(topic) {
        this.log('iot subscribe', topic);
        if (this.connection && this.connection.currentState === 0 && this.connection.desiredState === 0 && typeof topic === 'string' && topic.trim() !== '') {
            this.subscribedTopics.add(topic);
            return this.connection.subscribe(topic, mqtt.QoS.AtLeastOnce)
                .catch(err => {
                    this.log('Error subscribing to topic:', err);
                    this.subscribedTopics.delete(topic);
                    return Promise.reject(err);
                });
        } else {
            this.log('Invalid topic or not connected:', topic);
            return Promise.reject(new Error('Invalid topic or not connected'));
        }
    }

    unsubscribe(topic) {
        this.log('iot unsubscribe', topic);
        if (this.connection && this.connection.currentState === 0 && this.connection.desiredState === 0 && typeof topic === 'string' && topic.trim() !== '') {
            this.subscribedTopics.delete(topic);
            return this.connection.unsubscribe(topic)
                .catch(err => {
                    this.log('Error unsubscribing from topic:', err);
                    this.subscribedTopics.add(topic);
                    return Promise.reject(err);
                });
        } else {
            this.log('Invalid topic or not connected:', topic);
            return Promise.reject(new Error('Invalid topic or not connected'));
        }
    }

    send(topic, message) {
        this.log('iot send', topic, message);
        let msg = typeof message === 'object' ? JSON.stringify(message) : message;
        if (this.connection && this.connection.currentState === 0 && this.connection.desiredState === 0) {
            this.connection.publish(topic, msg, mqtt.QoS.AtLeastOnce, false);
        } else {
            throw new Error('Invalid topic or not connected:', topic);
        }
    }

    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'
                ) {
                    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);
        // disconnect is part of connect process
        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.connection || !(this.connection.currentState === 0 && this.connection.desiredState === 0)) {
                reject(new Error('Not connected'));
                return;
            }

            if (this.subscribedTopics.size === 0) {
                reject(new Error('No subscribed topics available for connection check', {cause: -1}));
                return;
            }

            // Find a suitable topic for the connection check
            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);
                reject(new Error('Connection check timeout'));
            }, 5000); // 5 seconds timeout

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

            this.on('message', checkMessageHandler);

            try {
                this.send(suitableTopic, testMessage);
            } catch (error) {
                clearTimeout(timeoutId);
                this.off('message', checkMessageHandler);
                reject(new Error(`Publish error: ${error.message}`));
            }
        });
    }
}

export default Iot;

