const SimplePeer = require('simple-peer');
// const net = require('net');
const crypto = require('crypto');
const childProcess = require('child_process');
const sip = require('jssip');
const { checkInitOptions, updateSettings, updateSettingsComposition } = require('./utils.js');

const EventEmitter = require('events');
class emitterClass extends EventEmitter {}
const emitter = new emitterClass(); //транслятор событий
//транслятор событий для операторской (события для работы с DOM и т.д.)
const providerEmitter = new emitterClass();

let webSocket = null; //WebSocket соединение с сигнальным сервером
let webSocketReconnectTimer = null; //Интервал переподключения к сигнальному серверу

let peer = null; //Peer соединение технологии WebRTC
let sipSession; // sip-сессия

//данные сервера (передаются в параметре options, при вызове функции init)
let serverIp = null;
let serverPort = null;

let isCalling = false; // идет ли вызов
let signalServerConnect = false; // проверка подключения с сигнальным сервером
let selfVideoStream = null; //наш видеострим
let receiverPayload = null; //данные для подключения к клиенту

//настройки провайдера, расширяющие глобальный объект settings
const providerSettings = {
    session: true,
    busy: false,
    incomingCall: false,
    receiver: null,
    count: 0, //количество принятых звонков
};
//настройки ресивера, расширяющие глобальный объект settings
const receiverSettings = {
    provider: null, //индентификатор назначеного оператора
    name: '', //наименование устройства
    camSet: {
        //настройки селф камеры (поворот, инвертация)
        rotate: 'none',
        invertX: false,
        invertY: false,
    },
    floor: 1,
    location: {
        x: 100,
        y: 100,
    },
    orientation: {
        x: 0,
        y: 1,
    },
    photoCaller: null,
    timeCallStarted: null, //время, в которое начал звонок оператор (для очереди)
};
//настройки, изначально содержит только общие для provider и receiver
const settings = {
    id: crypto.randomBytes(64).toString('hex'), //наш id
    type: 'receiver', //тип клиента (оператор (provider) или клиент (receiver))
    platform: {
        //платформа на которой установлено устройство
        id: '',
        name: '',
    },
    microphoneSensitivity: 65, //чувствительность нашего микрофона
    reconnectTime: 15, //Время следующей поппытки подключения к серверу
    stunServer: 'stun.neuro-city.ru',
};
//конфиг, используется при создании пира
const peerConfig = {
    initiator: true, //если инициализируем в качестве провайдера, это поле удаляется
    trickle: false,
    iceTransportPolicy: 'relay',
    reconnectTimer: 10000,
    config: {
        iceServers: [
            {
                urls: `stun:${settings.stunServer}`,
            },
        ],
    },
    offerConstraints: {
        offerToReceiveAudio: true,
        offerToReceiveVideo: true,
    },
    stream: selfVideoStream,
};

//Отправляет информацию на сервер
const sendInfo = () => {
    webSocket.send(
        JSON.stringify({
            action: 'info',
            data: settings,
        }),
    );
};

//Отправляем инфо в качестве ресивера (клиента)
const sendReceiverInfo = (signal) => {
    if (!webSocket || webSocket.readyState !== 1 || !signalServerConnect) {
        emitter.emit('call-not-possible', 'Нет подключения к сигнальному серверу');
        return;
    }
    settings.payload = signal;
    webSocket.send(
        JSON.stringify({
            action: 'signal-from-receiver',
            data: settings,
        }),
    );
    settings.payload = null;
    settings.photoCaller = null;
};

//Создаём пира (клиент), устанавливаем обработчики событий
const makeClientPeer = () => {
    const peer = new SimplePeer(peerConfig);
    peer.on('signal', (signal) => {
        emitter.emit(
            'info',
            `Установлено соединение со STUN-сервером "${settings.stunServer}". Ожидается подключение собеседника`,
        );
        //Если мы клиент, отправляем данные о нашем местоположении
        if (settings.type === 'receiver') sendReceiverInfo(signal);
        //Запускаем настройку микрофона фронтальной камеры
        if (process.platform === 'linux') {
            childProcess.execSync(
                `pactl set-source-volume @DEFAULT_SOURCE@ ${settings.microphoneSensitivity - 10}%`,
            );
            childProcess.execSync(
                `pactl set-source-volume @DEFAULT_SOURCE@ ${settings.microphoneSensitivity}%`,
            );
        }
    });
    peer.on('connect', () => {
        settings.timeCallStarted = null; // обнуляем время начала вызова, так как оператор уже ответил
        sendInfo();
        emitter.emit('info', 'Успешно соединились с собеседником');
    });
    peer.on('stream', (stream) => {
        emitter.emit('info', 'Начат приём медиастрима собеседника');
        emitter.emit('incoming-stream', stream);
    });
    peer.on('data', (data) => {
        switch (data.toString()) {
            case 'muted':
                emitter.emit('mute');
                break;
            case 'hideVideo':
                emitter.emit('hide-video');
                break;
            default:
                //Данный кейс необходим для отправки нового значения которое изменяет чувствительность микрофона во время разговора
                const newMicVolumeValue = Number(data.toString());
                if (Number.isNaN(newMicVolumeValue) === false && newMicVolumeValue <= 1) {
                    emitter.emit('change-volume', Number(data.toString()));
                }
                break;
        }
    });
    peer.on('error', (error) => {
        emitter.emit('error', 'Ошибка подключения к STUN-серверу: ' + error.message);
    });
    peer.on('close', () => {
        emitter.emit('call-ended');
        emitter.emit(
            'info',
            `Соединение со STUN-сервером "${settings.stunServer}" успешно закрыто`,
        );
    });
    return peer;
};

//Создаём пира (провайдер), устанавливаем обработчики событий
const makeProviderPeer = () => {
    peer = new SimplePeer(peerConfig);
    peer.on('signal', (signal) => {
        webSocket.send(
            JSON.stringify({
                action: 'signal-from-provider',
                data: {
                    id: settings.id,
                    receiver: settings.receiver,
                    payload: signal,
                },
            }),
        );
    });
    peer.on('connect', () => {
        settings.count += 1; //увеличиваем счётчик принятых звонков у оператора
        emitter.emit('info', 'Установлено соединение');
        providerEmitter.emit('accept-call');
    });
    peer.on('data', (data) => {
        emitter.emit('info', data);
    });
    peer.on('error', (err) => {
        if (err.message === 'Ice connection failed') {
            emitter.emit('error', 'Обрыв связи simple-peer');
        } else {
            emitter.emit('error', err);
        }
    });
    peer.on('close', () => {
        emitter.emit('info', 'Peer соединение закрыто');
        providerEmitter.emit('call-ended');
    });
    peer.on('stream', (stream) => {
        emitter.emit('info', 'Принят стрим от абонента');
        providerEmitter.emit('stream', stream);
    });
    peer.signal(receiverPayload);
};

//создаёт и возвращает наружу sipSocket
const makeSip = (params) => {
    const { serverUrl, uri, username, password } = params;
    const sipSocket = new sip.UA({
        sockets: [new sip.WebSocketInterface(serverUrl)],
        uri,
        authorization_user: username,
        password,
    });
    sipSocket.on('connected', () => sipSocket.register());
    sipSocket.on('disconnected', (e) =>
        emitter.emit('error', 'Потеряно соединение с сервером. ' + e.reason),
    );
    sipSocket.start();
    return sipSocket;
};

//функция обработки сообщений сервера для клиента (receiver)
const onMessageReceiver = async (event) => {
    console.log('Server response: ', event);
    console.log('Parsed data: ', JSON.parse(event.data));
    try {
        const data = JSON.parse(event.data);
        switch (data.action) {
            //Сигнал о постановки клиента на обслуживание
            case 'served':
                {
                    signalServerConnect = true;
                    emitter.emit('inited', settings);
                }
                break;
            //Сигнал о найденом операторе
            case 'provider-found':
                {
                    emitter.emit('info', 'Оператор подобран');
                    settings.provider = data.data.id;
                    //Как только получили и запомнили назначенного нам оператора, сообщаем об этом серверу
                    sendInfo();
                    peer = makeClientPeer();
                }
                break;
            //Сигнал о принятии вызова
            case 'signal-to-receiver':
                {
                    emitter.emit('info', 'Оператор ответил');
                    peer.signal(data.data.payload);
                }
                break;
            //Подобран занятой оператор
            case 'found-await-provider':
                {
                    emitter.emit('busy-operator-found', ++data.data.queue);
                }
                break;
            //Нет свободных операторов
            case 'no-free-devices':
                {
                    emitter.emit('busy', data?.data?.message || 'Все операторы платформы заняты');
                }
                break;
            //Оператор сбросил вызов
            case 'provider-is-busy':
                {
                    emitter.emit('call-decline');
                    settings.provider = null;
                    settings.timeCallStarted = null;
                    sendInfo();
                    if (peer) {
                        peer.destroy();
                        peer = null;
                    }
                }
                break;
            //Очередь обновлена
            case 'update-queue':
                {
                    emitter.emit('update-queue', data.data.queue);
                }
                break;
            //Нет ни одного оператора
            case 'there-are-not-operators':
                {
                    emitter.emit(
                        'call-not-possible',
                        `Нет ни одного свободного оператора платформы "${settings.platform.name}"`,
                    );
                }
                break;
            // звонок колл-центру
            case 'call-center-found': {
                const { serverUrl, uri, username, password, destination } = data.data;

                const sipSocket = makeSip({ serverUrl, uri, username, password });
                const audioStream = new Audio();

                navigator.mediaDevices.getUserMedia({ audio: true }).then((media) => {
                    emitter.emit('info', `Звонок ${destination} с аккаунта ${uri}`);

                    if (!isCalling) {
                        this.declineCall();
                        return;
                    }

                    sipSession = sipSocket.call(destination, {
                        eventHandlers: {
                            confirmed: () => {
                                emitter.emit('info', 'Успешно соеденились с колл-центром');
                                emitter.emit('incoming-audio', audioStream.srcObject);
                            },
                            ended: () => {
                                emitter.emit('call-ended');
                                sipSocket.terminateSessions();
                                webSocket.send(
                                    JSON.stringify({
                                        action: 'decline-call',
                                        data: {
                                            myId: settings.id,
                                        },
                                    }),
                                );
                            },
                            failed: (e) => {
                                if (
                                    ['Busy', 'Rejected', 'Redirected', 'Unavailable'].includes(
                                        e.cause,
                                    )
                                ) {
                                    emitter.emit(
                                        'warn',
                                        'Не получилось установить соединение с колл-центром. Причина: ' +
                                            e.cause,
                                    );
                                } else if (!['Canceled'].includes(e.cause)) {
                                    emitter.emit(
                                        'error',
                                        'Произошла ошибка во время звонка колл-центру. Причина: ' +
                                            e.cause,
                                    );
                                }
                                emitter.emit('call-ended');
                                sipSocket.terminateSessions();
                                this.declineCall();
                            },
                            connecting: () =>
                                emitter.emit('info', 'Устанавливается соединение с сервером'),
                            progress: () => {
                                sipSession.connection.ontrack = (e) => {
                                    audioStream.srcObject = e.streams[0];
                                };
                            },
                        },
                        mediaStream: media,
                        mediaConstraints: {
                            audio: true,
                            video: false,
                        },
                    });
                    sipSession.data = username;
                });
                break;
            }
            // колл-центр не доступен
            case 'call-center-unavailable': {
                emitter.emit('error', 'Колл-центр недоступен');
                emitter.emit('call-ended');
                this.declineCall();
                break;
            }
            // все номера заняты, просьба подождать
            case 'all-numbers-busy':
                {
                    emitter.emit('wait-number');
                    break;
                }
                settings;
            default:
                emitter.emit('error', 'Неизвестный сигнал сервера: ' + data.action);
        }
    } catch (error) {
        isCalling = false;
        emitter.emit('error', 'Не удалось обработать ответ сервера. Ошибка:' + error.message);
    }
};

//функция отправки события, если оператор занят
const sendProviderIsBusy = (id) => {
    webSocket.send(
        JSON.stringify({
            action: 'provider-is-busy',
            data: {
                id,
            },
        }),
    );
};

//функция обработки сообщений сервера для оператора (provider)
const onMessageProvider = async (event) => {
    try {
        const data = JSON.parse(event.data);
        switch (data.action) {
            // case 'served': // Сообщение о подключении к сигнальному серверу
            //     break;
            // Сигнал оператору
            case 'signal-to-provider': {
                if (settings.busy) {
                    sendProviderIsBusy(data.data.id);
                    return;
                }
                settings.receiver = data.data.id;
                receiverPayload = data.data.payload;
                providerEmitter.emit('signal-to-provider', JSON.stringify(data));
                break;
            }
            // Фотография клиента из очереди
            // case 'photo-caller':
            //     {
            //         emitter.emit('info', 'Пришла фотография абонента из очереди');
            //         console.log(data);
            //     }
            //     break;
            case 'decline-call': {
                emitter.emit('info', 'Клиент сбросил вызов');
                providerEmitter.emit('receiver-declined-call');
                settings.incomingCall = false;
                settings.busy = false;
                sendInfo();
                break;
            }
            //Иформация о количестве клиентов в очереди от сервера
            case 'queue-size': {
                providerEmitter.emit('queue-size', data.data.queueSize);
                break;
            }
        }
    } catch (error) {
        emitter.emit('error', 'Не удалось обработать ответ сервера. Ошибка:' + error.message);
    }
};

//Переподключение соединения с сервером в случае обрыва
const reconnectWS = (reject) => {
    if (reject.currentTarget.readyState === 3 && !webSocketReconnectTimer) {
        signalServerConnect = false;
        if (peer) {
            peer.destroy();
            peer = null;
        }
        webSocketReconnectTimer = setInterval(() => {
            emitter.emit(
                'error',
                `Потеряно соединение с сервером ${serverIp}:${serverPort}, повторное подключение через ${settings.reconnectTime} секунд`,
            );
            settings.type === 'receiver'
                ? webSocket.removeEventListener('message', (event) => onMessageReceiver(event))
                : webSocket.removeEventListener('message', (event) => onMessageProvider(event));
            webSocket.removeEventListener('close', (event) => reconnectWS(event));
            webSocket.removeEventListener('error', (event) => reconnectWS(event));
            webSocket = new window.WebSocket('ws://' + serverIp + ':' + serverPort);
            webSocket.addEventListener('open', (state) => {
                if (state.currentTarget.readyState === 1) {
                    clearInterval(webSocketReconnectTimer);
                    webSocketReconnectTimer = null;
                    // serverConnect = true;
                    emitter.emit(
                        'info',
                        `Соединение с сервером ${serverIp}:${serverPort} восстановлено`,
                    );
                    sendInfo();
                    settings.type === 'receiver'
                        ? webSocket.addEventListener('message', (event) => onMessageReceiver(event))
                        : webSocket.addEventListener('message', (event) =>
                              onMessageProvider(event),
                          );
                    webSocket.addEventListener('close', (event) => reconnectWS(event));
                    webSocket.addEventListener('error', (event) => reconnectWS(event));
                }
            });
        }, settings.reconnectTime * 1000);
    }
};

//Передача событий и данных из модуля
exports.on = (event, action) => emitter.on(event, (data) => action(data));

//Инициализация модуля
exports.init = (options) => {
    //Проверка входных параметров
    if (typeof options !== 'object') {
        emitter.emit(
            'error',
            'Инициализация модуля NeuroPhone невозможна. Не заданы обязательные параметры при инициализации',
        );
        return;
    }
    const errorsList = checkInitOptions(options);
    if (errorsList !== '') {
        emitter.emit(
            'error',
            'Инициализация модуля NeuroPhone невозможна. Неверно заданы следующие обязательные параметры:' +
                errorsList,
        );
        return;
    }

    //записываем данные сервера в глобальные переменные
    serverIp = options.serverIp;
    serverPort = options.serverPort;

    //initiator: true только у клиента (ресивера), добавляем оператору имя
    if (options.clientType === 'provider') delete peerConfig.initiator;
    if (options.clientType === 'provider') settings.name = options.name;

    //добавляет новые поля в settings, в зависимости от типа типа клиента (receiver или provider)
    updateSettingsComposition(settings, options.clientType, receiverSettings, providerSettings);
    //Устанавливаем новые значения settings (если новые значения переданы в options и валидны)
    updateSettings(settings, options);

    if (
        options.clientType === 'receiver' &&
        settings.camSet.rotate === 'none' &&
        settings.camSet.invertX === false &&
        settings.camSet.invertY === false
    ) {
        emitter.emit(
            'warn',
            'Не заданы настройки камеры. Видео пользователя может отображаться некорректно у оператора',
        );
    }

    webSocket = new window.WebSocket('ws://' + options.serverIp + ':' + options.serverPort);
    settings.type === 'receiver'
        ? webSocket.addEventListener('message', (event) => onMessageReceiver(event))
        : webSocket.addEventListener('message', (event) => onMessageProvider(event));
    //Мгновенно срабатывает при коннекте к серверу
    webSocket.addEventListener('open', () => {
        serverConnect = true;
        sendInfo();
    });
    webSocket.addEventListener('close', (event) => reconnectWS(event));
    webSocket.addEventListener('error', (event) => reconnectWS(event));
};

//Позволяет задать новые настройки камеры
exports.setCam = (options) => {
    if (
        options.camRotate === 'none' ||
        options.camRotate === 'left' ||
        options.camRotate === 'right' ||
        options.camRotate === 'invert'
    )
        settings.camSet.rotate = options.camRotate;
    if (typeof options.camInvertX === 'boolean') settings.camSet.invertX = options.camInvertX;
    if (typeof options.camInvertY === 'boolean') settings.camSet.invertY = options.camInvertY;
};

//Позволяет задать чувствительность микрофона
exports.setMicrophoneSensitivity = (value) => {
    if (
        value &&
        value !== '' &&
        Number(value) != NaN &&
        Number.isInteger(Number(value)) &&
        Number(value) >= 0 &&
        Number(value) <= 100
    ) {
        console.log('новая чувствительность микрофона: ', value);
        settings.microphoneSensitivity = value;
        // sendInfo();
    } else {
        emitter.emit(
            'error',
            'Не удалось установить чувствительность микрофона. Параметр должен быть целым числов в диапазоне от 0 до 100',
        );
    }
};

//Позволяет задать наш стрим с камеры
exports.setSelfStream = (stream) => {
    if (stream && stream.active === true) {
        selfVideoStream = stream;
        peerConfig.stream = stream;
    } else {
        selfVideoStream = null;
    }
};

//Делает запрос на сервер на поиск оператора
exports.call = (isOuterCallCenter = true, photo = null) => {
    //устанавливаем время начала вызова, передаём инфо на сервер
    settings.timeCallStarted = Date.now();
    sendInfo();
    isCalling = true;
    if (!webSocket || webSocket.readyState !== 1 || !signalServerConnect) {
        emitter.emit('call-not-possible', 'Нет подключения к сигнальному серверу');
        return;
    }

    settings.photoCaller = photo;
    if (isOuterCallCenter || (selfVideoStream && selfVideoStream.active === true)) {
        webSocket.send(
            JSON.stringify({
                action: 'looking-for-provider',
                data: {
                    id: settings.id,
                    isOuterCallCenter,
                },
            }),
        );
    } else {
        emitter.emit('call-not-possible', 'Стрим с фронтальной камеры неактивен');
    }
};

//Завершает вызов
exports.declineCall = () => {
    isCalling = false;
    if (!webSocket || webSocket.readyState !== 1 || !signalServerConnect) {
        emitter.emit('call-not-possible', 'Нет подключения к сигнальному серверу');
    }
    //если функцию сброса вызова использует клиент
    if (settings.type === 'receiver') {
        //устанавливаем timeCallStarted в null, так как мы перестаём звонить
        settings.timeCallStarted = null;
        sendInfo();
        webSocket.send(
            JSON.stringify({
                action: 'decline-call',
                data: {
                    id: settings.provider,
                    myId: settings.id,
                },
            }),
            );
        settings.provider = null;
    }
    //если функцию сброса использует оператор
    if (settings.type === 'provider') {
        //сбрасываем вызов: отправляем серверу инфо о том, что оператор занят
        sendProviderIsBusy();
    }

    if (peer) {
        peer.destroy();
        peer = null;
    }

    if (sipSession) {
        try {
            sipSession.terminate({ cause: 'Terminated' });
        } catch (error) {
            console.error(error);
        }

        sipSession = null;
    }

    //если функцию сброса использует оператор
    if (settings.type === 'provider') {
        setTimeout(() => {
            //отправляем серверу инфо о том, что оператор свободен, чтобы снова принимать вызовы
            settings.incomingCall = false;
            settings.busy = false;
            sendInfo();
        }, 2000);
    }
    
};

//Оператор принимает вызов
exports.acceptConnectionProvider = () => {
    settings.incomingCall = false;
    settings.busy = true;
    sendInfo();
    makeProviderPeer();
};

exports.changePlatform = (platformName, platformId) => {
    if (!platformName) {
        emitter.emit('error', 'Не задано имя для смены платформы');
        return;
    }
    if (typeof platformName !== 'string') {
        emitter.emit('error', 'В новом имени платформы передана не строка');
        return;
    }
    if (!platformId) {
        emitter.emit('error', 'Не задан id для смены платформы');
        return;
    }
    if (typeof platformId !== 'string') {
        emitter.emit('error', 'В id новой платформы передана не строка');
        return;
    }
    settings.platform.id = platformId;
    settings.platform.name = platformName;
    sendInfo();
};

exports.onProviderEvents = (event, action) => providerEmitter.on(event, (data) => action(data));
exports.sendProviderIsBusy = sendProviderIsBusy;

//функция возвращает true если оператор не занят 
exports.offProvider = () => {
    if (!settings.busy) {
        settings.incomingCall = false;
        settings.busy = true;
        sendInfo();
        return true;
    }
};

//подключения оператора
exports.onProvider = () => {
    settings.incomingCall = false;
    settings.busy = false;
    sendInfo();
};

//отключение микрофона
exports.toggleMic = () => {
    if (peer) {
        peer.send('muted');
    }
};

//отключение видеотрансляции
exports.toggleVideo = () => {
    if (peer) {
        peer.send('hideVideo');
    }
};

// изменение громкости микрофона во время разговора
exports.controlMicVolume = (volume) => {
    peer.send(volume);
};
