// tslint:disable-next-line: match-default-export-name
import Axios from 'axios';
import { EventEmitter } from 'ee-ts';
import { Log } from '../log';
import { SipPhone } from '../sip/webrtc';
import { SipPhone as ExperimentalPhone } from '../sip/webrtc2';
import { version } from '../version';
import { Agent, ChannelState } from './agent';
import { Call, CallActions, } from './call';
import { ChatActions, Conversation, } from './conversation';
import { DeviceNotAllowPermissionError, DeviceNotFoundError, LicencePermissionError, RolePermissionError, TypeErrors, } from './errors';
import { NotificationActions, } from './notification';
import { Socket } from './socket';
import { ChannelName, JobState, Task, } from './task';
import { formatBaseUri, generateId } from './utils';
import { SenderSession } from '../screen';
import { SpyScreen } from './screen';
// Назва заголовку для токена доступу
const API_HEADER_TOKEN = 'X-Webitel-Access';
// Тип події WebSocket для аутентифікації
const WEBSOCKET_AUTHENTICATION_CHALLENGE = 'authentication_challenge';
// Конфігурація пристрою за замовчуванням для користувача через WebSocket
const WEBSOCKET_DEFAULT_DEVICE_CONFIG = 'user_default_device';
// Подія WebSocket для сесії агента контакт-центру
const WEBSOCKET_AGENT_SESSION = 'cc_agent_session';
// Подія WebSocket для перевірки підключення (ping)
const WEBSOCKET_PING = 'ping';
// Подія WebSocket для створення вихідного дзвінка
const WEBSOCKET_MAKE_OUTBOUND_CALL = 'call_invite';
// Подія WebSocket для виклику користувача
const WEBSOCKET_MAKE_USER_CALL = 'call_user';
// Подія WebSocket для ініціалізації (hello)
const WEBSOCKET_EVENT_HELLO = 'hello';
// Подія WebSocket для обробки дзвінків
const WEBSOCKET_EVENT_CALL = 'call';
// Подія WebSocket для обробки чатів
const WEBSOCKET_EVENT_CHAT = 'chat';
// Подія WebSocket для відстеження стану користувача
const WEBSOCKET_EVENT_USER_STATE = 'user_state';
// Подія WebSocket для оновлення статусу агента
const WEBSOCKET_EVENT_AGENT_STATUS = 'agent_status';
// Подія WebSocket для оновлення стану каналу зв'язку
const WEBSOCKET_EVENT_CHANNEL_STATUS = 'channel';
// Подія WebSocket для обробки додавання учасника в чергу
const WEBSOCKET_EVENT_QUEUE_JOIN_MEMBER = 'queue_join_member';
// Подія WebSocket для обробки помилок
const WEBSOCKET_EVENT_ERROR = 'error';
// Подія WebSocket для обробки загальних повідомлень
const WEBSOCKET_EVENT_NOTIFICATION = 'notification';
export const WEBSOCKET_EVENT_CALL_MEDIA_METRIC = 'call_media_metric';
// Подія WebSocket для обробки завдань
const TASK_EVENT = 'task';
// Подія WebSocket для обробки робіт
const JOB_EVENT = 'job';
// Подія WebSocket для обробки SIP повідомлень
const WEBSOCKET_EVENT_SIP = 'sip';
// Подія WebSocket для обробки SDP повідомлень
const WEBSOCKET_EVENT_SDP = 'sdp';
/**
 * Помилки обробки.
 */
var HandleError;
(function (HandleError) {
    HandleError["NotFoundError"] = "NotFoundError";
    HandleError["NotAllowedError"] = "NotAllowedError";
})(HandleError || (HandleError = {}));
/**
 * Статус відповіді.
 */
export var Response;
(function (Response) {
    Response["STATUS_FAIL"] = "FAIL";
    Response["STATUS_OK"] = "OK";
})(Response || (Response = {}));
/**
 * Цей клас Client розширює EventEmitter, який використовується для керування подіями,
 * що описані в інтерфейсі ClientEvents. Він має декілька полів для збереження стану клієнта,
 * таких як agent, phone, lastError, lastLatency, а також деякі приватні поля для внутрішнього використання,
 * такі як socket, connectionInfo, reqSeq, queueRequest, log, eventHandler, callStore, conversationStore,
 * pingTimer, та toneTimer.
 * Конструктор класу приймає конфігураційні параметри, які використовуються для ініціалізації клієнта.
 */
export class Client extends EventEmitter {
    /**
     * Конструктор для створення екземпляра клієнта.
     *
     * @param _config - Конфігураційні параметри клієнта.
     */
    constructor(_config) {
        super();
        this._config = _config;
        this.reqSeq = 0;
        this.queueRequest = new Map();
        this.ackQueue = new Map();
        this.log = new Log(_config.logLvl);
        this.eventHandler = new EventEmitter();
        this.callStore = new Map();
        this.spyScreenSessions = [];
        this.senderScreenStore = [];
        this.conversationStore = new Map();
        this.pingTimer = null;
        this.toneTimer = null;
        this.basePath = `${formatBaseUri(_config.storageEndpoint || _config.endpoint)}`;
        this.lastError = null;
        this.lastLatency = null;
        this.screenResolver = _config.screenResolver || null;
    }
    async connect() {
        await this.connectToSocket();
    }
    async disconnect() {
        this.socket.close();
    }
    async subscribeCall(handler, data) {
        const calls = (await this.request(`subscribe_call`, data));
        this.eventHandler.on(WEBSOCKET_EVENT_CALL, handler);
        if (calls.items && calls.items.length) {
            for (const c of calls.items) {
                if (c.hangup_at && c.hangup_at > 0 && !c.task) {
                    continue;
                }
                const e = {
                    cc_app_id: '',
                    app_id: c.app_id,
                    event: c.state,
                    id: c.id,
                    timestamp: Date.now(),
                    data: {
                        app_id: c.app_id,
                        event: c.state,
                        id: c.id,
                        timestamp: Date.now(),
                        cc_app_id: '',
                        sip_id: '',
                        parent_id: c.parent_id,
                        hide_number: c.hide_number,
                        direction: c.direction,
                        destination: c.destination,
                        queue: c.queue,
                        from: c.from,
                        to: c.to,
                        payload: c.variables,
                    },
                };
                const call = new Call(this, e);
                call.createdAt = c.created_at;
                call.answeredAt = c.answered_at || 0;
                call.bridgedAt = c.bridged_at || 0;
                call.hangupAt = c.hangup_at || 0;
                // call.reportingAt = c.reporting_at || 0
                if (c.task) {
                    let task = this.getTask(c.task.attempt_id);
                    if (!task) {
                        task = new Task(this, {
                            attempt_id: c.task.attempt_id,
                            channel: c.task.channel,
                            status: c.task.status,
                            timestamp: Date.now(),
                        }, c.task);
                        task.form = c.task.form || null;
                        if (c.leaving_at && c.task.processing_sec) {
                            task.startProcessingAt = c.leaving_at;
                            task.setProcessing(c.leaving_at, {
                                sec: c.task.processing_sec || 0,
                                timeout: c.task.processing_timeout_at || null,
                                renewal_sec: c.task.processing_renewal_sec || 0,
                            });
                        }
                    }
                    call.task = task;
                    call.queue.reporting = 'true';
                }
                if (c.contact_id) {
                    call.setContactId(c.contact_id);
                }
                this.callStore.set(call.id, call);
            }
        }
        return calls;
    }
    /**
     * Підписується на події чату.
     *
     * @param handler - Обробник подій чату.
     * @param data - Додаткові дані для підписки (необов'язково).
     */
    async subscribeChat(handler, data) {
        const res = (await this.request(`subscribe_chat`, data));
        this.eventHandler.on(WEBSOCKET_EVENT_CHAT, handler);
        if (res && res.items) {
            for (const conv of res.items) {
                // tslint:disable-next-line: no-assign-mutated-array
                const messages = conv.messages.reverse();
                const c = new Conversation(this, conv.id, conv.title, conv.members, messages, conv.variables);
                c.createdAt = conv.created_at;
                c.closedAt = conv.closed_at;
                if (conv.task) {
                    let task = this.getTask(conv.task.attempt_id);
                    if (!task) {
                        task = new Task(this, {
                            attempt_id: conv.task.attempt_id,
                            channel: conv.task.channel,
                            status: conv.task.status,
                            timestamp: Date.now(),
                        }, conv.task // todo add app_id
                        );
                        if (conv.leaving_at && conv.task.processing_sec) {
                            task.startProcessingAt = conv.leaving_at;
                            task.setProcessing(conv.leaving_at, {
                                sec: conv.task.processing_sec || 0,
                                timeout: conv.task.processing_timeout_at || null,
                                renewal_sec: conv.task.processing_renewal_sec || 0,
                            });
                        }
                        task.form = conv.task.form || null;
                    }
                    c.task = task;
                }
                if (conv.invite_id) {
                    c.setInvite(conv.invite_id, conv.created_at);
                }
                else if (conv.channel_id) {
                    c.setAnswered(conv.channel_id, conv.joined_at, conv.member);
                    if (conv.closed_at) {
                        c.setClosed(conv.closed_at);
                    }
                }
                else {
                    this.log.error(`conversation ${conv.id} not valid`);
                    continue;
                }
                this.conversationStore.set(c.id, c);
            }
        }
        return res;
    }
    async subscribeUsersStatus(handler, data) {
        const res = await this.request(`subscribe_users_status`, data);
        this.eventHandler.on(WEBSOCKET_EVENT_USER_STATE, handler);
        return res;
    }
    async subscribeAgentsStatus(handler, data) {
        const res = await this.request(`cc_agent_subscribe_status`, data);
        this.eventHandler.on(WEBSOCKET_EVENT_AGENT_STATUS, handler);
        return res;
    }
    getTask(id) {
        if (!this.agent) {
            return undefined;
        }
        return this.agent.task.get(id);
    }
    subscribeTask(handler) {
        this.eventHandler.on(TASK_EVENT, handler);
    }
    subscribeJob(handler) {
        this.eventHandler.on(JOB_EVENT, handler);
    }
    async unSubscribe(action, handler, data) {
        return this.request(`un_subscribe_${action}`, data);
    }
    async destroy() {
        await this.disconnect();
        if (this.phone) {
            await this.phone.unregister();
            this.phone.off('*');
            delete this.phone;
        }
        this.eventHandler.off('*');
    }
    allCall() {
        return Array.from(this.callStore.values());
    }
    allConversations() {
        return Array.from(this.conversationStore.values());
    }
    // todo deprecated
    allTask() {
        if (!this.agent) {
            return [];
        }
        return Array.from(this.agent.task.values());
    }
    allJob() {
        return this.allTask().filter(isJobTask);
    }
    allSpyScreenSessions() {
        return this.spyScreenSessions;
    }
    callById(id) {
        if (this.callStore.has(id)) {
            return this.callStore.get(id);
        }
    }
    conversationById(id) {
        if (this.conversationStore.has(id)) {
            return this.conversationStore.get(id);
        }
    }
    callBySipId(id) {
        for (const call of this.allCall()) {
            if (call.sipId && id.startsWith(call.sipId)) {
                // FIXME
                return call;
            }
        }
    }
    async reportingTask(attemptId, reporting) {
        return this.request('cc_reporting', {
            attempt_id: attemptId,
            ...reporting,
        });
    }
    async agentActiveTasks(agentId) {
        return this.request(`cc_agent_tasks`, {
            agent_id: agentId,
        });
    }
    async auth() {
        try {
            const resp = await this.request(WEBSOCKET_AUTHENTICATION_CHALLENGE, {
                token: this._config.token,
            });
            return resp;
        }
        catch (err) {
            this.handleError(err);
            await this.disconnect();
            throw err;
        }
    }
    async ping() {
        const response = (await this.request(WEBSOCKET_PING, {
            ping: 1,
        }));
        if (response && response.server_ts) {
            const ack = {
                client_ts: 0,
                client_ack_ts: 0,
                server_ts: response.server_ts,
                server_ack_ts: 0,
            };
            return this.calculateLatency(ack);
        }
        return response;
    }
    sessionInfo() {
        return this.connectionInfo.session;
    }
    get version() {
        return this.connectionInfo.server_version;
    }
    get buildVersion() {
        return version;
    }
    get canUseChat() {
        const info = this.connectionInfo;
        return !!(info && info.use_chat);
    }
    get canUseCC() {
        const info = this.connectionInfo;
        return !!(info && info.use_cc);
    }
    get instanceId() {
        return this.connectionInfo.sock_id;
    }
    get autoAnswerDelayTime() {
        return this._config.autoAnswerDelayTime || 400;
    }
    async agentSession() {
        if (this.agent) {
            return this.agent;
        }
        const info = await this.request(WEBSOCKET_AGENT_SESSION);
        this.agent = new Agent(this, info);
        const taskList = (await this.agentActiveTasks(this.agent.agentId));
        // todo
        for (const t of taskList.items) {
            const task = new Task(this, {
                status: t.status,
                timestamp: Date.now(),
                channel: t.channel,
                attempt_id: t.attempt_id,
            }, t);
            task.postProcessData = {};
            task.setState(t.state);
            if (t.bridged_at) {
                task.bridgedAt = t.bridged_at;
                task.answeredAt = t.bridged_at;
            }
            if (t.leaving_at) {
                task.closedAt = t.leaving_at;
            }
            if (t.leaving_at && t.processing_sec) {
                task.startProcessingAt = t.leaving_at;
                task.setProcessing(t.leaving_at, {
                    sec: t.processing_sec || 0,
                    timeout: t.processing_timeout_at || null,
                    renewal_sec: t.processing_renewal_sec || 0,
                });
            }
            task.form = t.form || null;
            this.agent.task.set(task.id, task);
        }
        for (const call of this.allCall()) {
            if (call.task) {
                this.agent.task.set(call.task.id, call.task);
            }
        }
        for (const conv of this.allConversations()) {
            if (conv.task) {
                this.agent.task.set(conv.task.id, conv.task);
            }
        }
        return this.agent;
    }
    async invite(req) {
        return this.request(WEBSOCKET_MAKE_OUTBOUND_CALL, req);
    }
    async call(req) {
        if (this.phone) {
            try {
                await this.phone.call(req);
            }
            catch (e) {
                this.handleError(e);
            }
        }
        else {
            await this.invite(req);
        }
    }
    async callRecordId(id) {
        return this.request('call_recordings', { id });
    }
    async eavesdrop(req) {
        return this.request('call_eavesdrop', req);
    }
    async agentSetOnline(agentId, channels, onDemand) {
        return this.request('cc_agent_online', {
            agent_id: agentId,
            channels,
            on_demand: onDemand,
        });
    }
    async agentSetWaiting(agentId, channel) {
        return this.request('cc_agent_waiting', {
            agent_id: agentId,
            channel,
        });
    }
    async agentSetPause(agentId, payload) {
        return this.request('cc_agent_pause', {
            agent_id: agentId,
            payload,
        });
    }
    async agentSetOffline(agentId) {
        return this.request('cc_agent_offline', { agent_id: agentId });
    }
    inviteToUser(req) {
        return this.request(WEBSOCKET_MAKE_USER_CALL, req);
    }
    async answer(id, req) {
        const call = this.callById(id);
        if (call) {
            return call.answer(req);
        }
    }
    request(action, data) {
        // @ts-ignore
        return new Promise((resolve, reject) => {
            this.queueRequest.set(++this.reqSeq, { resolve, reject });
            this.socket.send({
                seq: this.reqSeq,
                action,
                data,
            });
        });
    }
    ackRequest(action, data, timeout) {
        // @ts-ignore
        return new Promise(
        // @ts-ignore
        async (resolve, reject) => {
            const id = `${generateId()}/${this.sessionInfo().user_id}/${this.instanceId}`;
            const ack = {
                resolve,
                reject,
                timerId: 0,
            };
            this.ackQueue.set(id, ack);
            try {
                await this.request(action, {
                    ...data,
                    ack_id: id,
                });
                if (timeout && timeout > 0) {
                    ack.timerId = setTimeout(() => {
                        this.ackQueue.delete(id);
                        reject(new Error('timeout'));
                    }, timeout);
                }
            }
            catch (e) {
                this.ackQueue.delete(id);
                reject(e);
            }
        });
    }
    useWebPhone() {
        return this._config.registerWebDevice || false;
    }
    callDestroyed(call) {
        return call.hangupAt > 0 && !this.hasAgentTask(call.task);
    }
    conversationDestroyed(conv) {
        return (conv.closedAt > 0 && (!this.hasAgentTask(conv.task) || !conv.hasReporting));
    }
    reportingChannelTask(task) {
        switch (task.channel) {
            case ChannelName.Call:
            case ChannelName.OutCall:
                for (const call of this.allCall()) {
                    if (this.callDestroyed(call)) {
                        this.destroyCall(call);
                        return;
                    }
                }
                break;
            case ChannelName.Chat:
                if (task.agentChannelId) {
                    const conv = this.conversationById(task.agentChannelId);
                    if (conv && this.conversationDestroyed(conv)) {
                        this.destroyConversation(conv);
                        return;
                    }
                }
                break;
            default:
        }
    }
    async registerCallClient(phone) {
        this.phone = phone;
        this.subscribePhone(phone);
        try {
            const conf = await this.deviceConfig(this.phone.type);
            await this.phone.register(conf);
        }
        catch (e) {
            this.emit('error', e);
        }
    }
    subscribePhone(phone) {
        phone.on('peerStreams', (session, streams) => {
            const call = this.callBySession(session);
            if (call && !call.peerStreams.length) {
                call.setPeerStreams(streams);
                this.eventHandler.emit(WEBSOCKET_EVENT_CALL, CallActions.PeerStream, call);
            }
        });
        phone.on('localStreams', (session, streams) => {
            const call = this.callBySession(session);
            if (call && !call.localStreams.length) {
                call.setLocalStreams(streams);
                this.eventHandler.emit(WEBSOCKET_EVENT_CALL, CallActions.LocalStream, call);
            }
        });
        phone.on('newSession', this.onNewCallSession.bind(this));
        phone.on('info', this.onCallInfoSession.bind(this));
        phone.on('registered', () => this.emit('phone_registered', true));
        phone.on('connected', () => this.emit('phone_connected', true));
        phone.on('unregistered', () => this.emit('phone_registered', false));
    }
    phoneIsRegister() {
        if (this.phone && this.phone.isRegistered) {
            return this.phone.isRegistered();
        }
        return false;
    }
    async deviceConfig(name) {
        return this.request(WEBSOCKET_DEFAULT_DEVICE_CONFIG, { name });
    }
    async storeFile(id, files, cb, channel) {
        if (!files || files.length < 1) {
            throw new Error('no files');
        }
        const formData = new FormData();
        for (const file of files) {
            formData.append(file.name, file); // todo name
        }
        let url = `${this.basePath}/api/storage/file/${id}/upload`;
        if (channel) {
            url += `?channel=${channel}`;
        }
        const result = await Axios.post(url, formData, {
            headers: {
                [API_HEADER_TOKEN]: this._config.token,
            },
            onUploadProgress: cb,
        });
        const data = result.data;
        for (const f of data) {
            f.shared = this.basePath + f.shared;
        }
        return data;
    }
    fileUrlDownload(fileId, mime) {
        const params = this.fileUrlParams(mime);
        return `${this.basePath}/api/storage/file/${fileId}/download?${params.join('&')}`;
    }
    fileUrlStream(fileId, mime) {
        const params = this.fileUrlParams(mime);
        return `${this.basePath}/api/storage/file/${fileId}/stream?${params.join('&')}`;
    }
    handleError(e) {
        if (e) {
            switch (e.name) {
                case HandleError.NotFoundError:
                    this.emit('error', new DeviceNotFoundError(e.message));
                    break;
                case HandleError.NotAllowedError:
                    this.emit('error', new DeviceNotAllowPermissionError(e.message));
                    break;
                default:
                    if (e.name !== 'INVALID_STATE_ERROR') {
                        // TODO: DEV-3409
                        this.emit('error', e);
                    }
            }
        }
    }
    destroyConversation(conv) {
        // FIXME sync channel & call event
        this.conversationStore.delete(conv.id);
        this.eventHandler.emit(WEBSOCKET_EVENT_CHAT, ChatActions.Destroy, conv);
    }
    removeSpyScreen(id) {
        const idx = this.spyScreenSessions.findIndex((i) => i.id === id);
        if (idx !== -1) {
            this.spyScreenSessions.splice(idx, 1);
            return true;
        }
        return false;
    }
    async spyScreen(agentId, conf, cb, timeout) {
        let rtcConf = conf;
        if (this.connectionInfo.rtc_configuration) {
            rtcConf = this.connectionInfo.rtc_configuration;
        }
        const s = new SpyScreen(this, agentId, rtcConf, this.log);
        s.on('close', () => {
            this.removeSpyScreen(s.id);
        });
        s.on('stream', cb);
        this.spyScreenSessions.push(s);
        try {
            const offer = await s.offer();
            await this.ackRequest(`ss_invite`, {
                id: s.id,
                sdp: offer.sdp,
                to_user_id: agentId,
            }, timeout || 10000); // TODO fix timeout
        }
        catch (e) {
            this.removeSpyScreen(s.id);
            this.log.error('error ', e);
            throw e;
        }
        return;
    }
    async onMessage(message) {
        this.log.debug('receive message: ', message);
        if (message.seq_reply > 0) {
            if (this.queueRequest.has(message.seq_reply)) {
                const promise = this.queueRequest.get(message.seq_reply);
                this.queueRequest.delete(message.seq_reply);
                if (message.status === Response.STATUS_OK) {
                    promise.resolve(message.data);
                }
                else {
                    const err = message.error;
                    if (err && err.id === TypeErrors.RolePermission) {
                        promise.reject(new RolePermissionError(err.detail));
                    }
                    else if (err && err.id === TypeErrors.LicencePermission) {
                        promise.reject(new LicencePermissionError(err.detail));
                    }
                    else {
                        promise.reject(message.error);
                    }
                }
            }
        }
        else {
            switch (message.event) {
                case WEBSOCKET_EVENT_HELLO:
                    await this.connected(message.data);
                    this.log.debug(`opened session ${this.connectionInfo.sock_id} for userId=${this.connectionInfo.session.user_id}`);
                    break;
                case WEBSOCKET_EVENT_CALL:
                    await this.handleCallEvents(message.data.call);
                    break;
                case WEBSOCKET_EVENT_CHAT:
                    await this.handleChatEvents(message.data);
                    break;
                case WEBSOCKET_EVENT_USER_STATE:
                    this.handleUserStateEvent(message.data.state);
                    break;
                case WEBSOCKET_EVENT_SIP:
                    this.eventHandler.emit(WEBSOCKET_EVENT_SIP, message.data);
                    break;
                case WEBSOCKET_EVENT_SDP:
                    this.emit(WEBSOCKET_EVENT_SDP, message.sock_id, message.data);
                    break;
                case WEBSOCKET_EVENT_AGENT_STATUS:
                    this.handleAgentStatus(message.data);
                    break;
                case WEBSOCKET_EVENT_CHANNEL_STATUS:
                    this.handleChannelEvents(message.data);
                    break;
                case WEBSOCKET_EVENT_ERROR:
                    this.lastError = message.data.error;
                    break;
                case WEBSOCKET_EVENT_NOTIFICATION:
                    await this.handleNotification(message.data
                        .notification);
                    break;
                default:
                    this.log.error(`event ${message.event} not handler`);
            }
        }
    }
    async handleNotification(e) {
        switch (e.action) {
            case NotificationActions.HideMember:
                if (this.agent && this.agent._listOfflineMembers) {
                    const list = this.agent._listOfflineMembers;
                    if (list.items) {
                        for (let i = 0; i < list.items.length; i++) {
                            if (list.items[i].id === e.body.member_id) {
                                list.items.splice(i, 1);
                                break;
                            }
                        }
                    }
                }
                break;
            case NotificationActions.SetContact:
                if (e.body) {
                    const contactId = e.body.contact_id;
                    const channel = e.body.channel;
                    switch (channel) {
                        case 'chat':
                            const conv = this.conversationById(e.body.id);
                            if (conv) {
                                conv.setContactId(contactId);
                            }
                            break;
                        default:
                            const call = this.callById(e.body.id);
                            if (call) {
                                call.setContactId(contactId);
                            }
                    }
                }
                break;
            case NotificationActions.WaitingList:
                if (this.agent) {
                    this.agent.setWaitingList(e.body);
                }
                break;
            case NotificationActions.HideAttempt:
                if (this.agent) {
                    this.agent.deleteWaitingAttempt(e.body);
                }
                break;
            case NotificationActions.RefreshMissed:
                this.emit(`refresh_missed`, e.body);
                break;
            case NotificationActions.ShowMessage:
                this.emit(`show_message`, e.body);
                break;
            case NotificationActions.OpenLink:
                this.emit(`open_link`, e.body);
                break;
            case NotificationActions.StartScreenRecord:
                const msgRecStart = e.body;
                this.emit(`screen_rec_start`, msgRecStart);
                if (msgRecStart.ack_id) {
                    await this.request('ss_ack', {
                        ack_id: msgRecStart.ack_id,
                    });
                }
                break;
            case NotificationActions.StopScreenRecord:
                const msgRecStop = e.body;
                this.emit(`screen_rec_stop`, msgRecStop);
                if (msgRecStop.ack_id) {
                    await this.request('ss_ack', {
                        ack_id: msgRecStop.ack_id,
                    });
                }
                break;
            case NotificationActions.Screenshot:
                const msgScreenshot = e.body;
                this.emit(`screenshot`, msgScreenshot);
                if (msgScreenshot.ack_id) {
                    await this.request('ss_ack', {
                        ack_id: msgScreenshot.ack_id,
                    });
                }
                break;
            case NotificationActions.ACK:
                const ackMsg = e.body;
                const ack = this.ackQueue.get(ackMsg.ack_id);
                if (ack) {
                    clearTimeout(ack.timerId);
                    this.ackQueue.delete(ackMsg.ack_id);
                    if (ackMsg.error) {
                        ack.reject(new Error(ackMsg.error));
                    }
                    else {
                        ack.resolve({});
                    }
                }
                break;
            case NotificationActions.ScreenShare:
                const body = e.body;
                switch (body.state) {
                    case 'invite':
                        if (!this.screenResolver) {
                            throw new Error('not found screenResolver');
                        }
                        let displayMediaStreamOptions = {};
                        let RTCConfigurationConf = {};
                        if (body.screenConstraints) {
                            displayMediaStreamOptions = body.screenConstraints;
                        }
                        if (body.RTCConfiguration) {
                            RTCConfigurationConf = body.RTCConfiguration;
                        }
                        else if (this.connectionInfo.rtc_configuration) {
                            RTCConfigurationConf = this.connectionInfo.rtc_configuration;
                        }
                        const stream = await this.screenResolver(displayMediaStreamOptions);
                        const s = new SenderSession(body.sdp, {
                            id: body.from_user_id,
                            sockId: body.sock_id,
                            sessionId: body.parent_id,
                        }, RTCConfigurationConf, this.log);
                        s.on('close', () => {
                            this.senderScreenStore = this.senderScreenStore.filter((i) => i.id !== s.id);
                        });
                        s.on('stream', async (rtc) => {
                            await this.request(`ss_accept`, {
                                id: s.id,
                                sdp: rtc.sdp,
                                to_user_id: s.from.id,
                                sock_id: s.from.sockId,
                                session_id: s.from.sessionId,
                            });
                        });
                        this.senderScreenStore.push(s);
                        // tslint:disable-next-line: no-floating-promises
                        s.start(stream);
                        if (body.ack_id) {
                            await this.request('ss_ack', {
                                ack_id: body.ack_id,
                            });
                        }
                        break;
                    case 'accept':
                        const receive = this.spyScreenSessions.filter((i) => i.id === body.session_id)[0];
                        if (receive) {
                            // tslint:disable-next-line: no-floating-promises
                            receive.answerSession(body.from_sock_id, {
                                type: 'answer',
                                sdp: body.sdp,
                            });
                        }
                        if (body.ack_id) {
                            await this.request('ss_ack', {
                                ack_id: body.ack_id,
                            });
                        }
                        break;
                    default:
                        this.log.error(`notification "${e.action}" not handled`);
                }
                break;
            default:
                this.log.error(`notification "${e.action}" not handled`);
        }
    }
    handleAgentStatus(e) {
        if (this.agent && e.agent_id === this.agent.agentId) {
            this.agent.setStatus(e);
        }
        this.eventHandler.emit(WEBSOCKET_EVENT_AGENT_STATUS, e, this.agent);
    }
    handleChannelEvents(e) {
        if (this.agent) {
            e.timestamp = Date.now(); // bug
            const task = this.agent.onChannelEvent(e) || undefined;
            this.eventHandler.emit(TASK_EVENT, e.status, task);
            if (task && isJobTask(task)) {
                this.eventHandler.emit(JOB_EVENT, task.state, task);
                if (isDestroyJob(task.state)) {
                    this.eventHandler.emit(JOB_EVENT, JobState.Destroy, task);
                }
            }
        }
    }
    deadlinePingTime(interval) {
        if (interval > 10000) {
            return interval / 2;
        }
        return 3000;
    }
    async pingServer() {
        if (this.pingTimer && this.connectionInfo.ping_interval) {
            const t = setTimeout(async () => {
                this.log.error('close socket without pong');
                this.socket.close(3005);
            }, this.deadlinePingTime(this.connectionInfo.ping_interval));
            await this.ping();
            clearTimeout(t);
        }
        if (this.connectionInfo.ping_interval) {
            // @ts-ignore
            this.pingTimer = setTimeout(this.pingServer.bind(this), +this.connectionInfo.ping_interval);
        }
    }
    async connected(info) {
        this.connectionInfo = info;
        if (info.ping_interval) {
            await this.pingServer();
        }
        if (!this.useWebPhone()) {
            return;
        }
        return this.registerCallClient(this.connectionInfo.b2bua
            ? new ExperimentalPhone(this)
            : new SipPhone(this.instanceId, this._config.debug));
    }
    async latency() {
        const ack = {
            client_ts: 0,
            client_ack_ts: 0,
            server_ts: 0,
            server_ack_ts: 0,
        };
        Object.assign(ack, await this.request(`latency_start`, ack));
        return this.calculateLatency(ack);
    }
    async calculateLatency(ack) {
        ack.client_ts = Date.now();
        Object.assign(ack, await this.request(`latency_ack`, {
            ...ack,
            last_latency: this.lastLatency || 0,
        }));
        ack.client_ack_ts = Date.now();
        this.lastLatency = Math.abs(ack.server_ack_ts -
            ack.server_ts -
            (ack.client_ack_ts - ack.client_ts) * 0.5);
        this.log.info(`[websocket] latency: ${this.lastLatency}`);
        return this.lastLatency;
    }
    callBySession(session) {
        for (const call of this.allCall()) {
            if (call.sip && call.sip.id === session.id) {
                return call;
            }
        }
    }
    onCallInfoSession(session) {
        const call = this.callBySession(session);
        if (call) {
            this.eventHandler.emit(WEBSOCKET_EVENT_CALL, CallActions.Info, call);
        }
    }
    async onNewCallSession(session) {
        let call;
        if (session.callId) {
            call = this.callById(session.callId);
        }
        else {
            call = this.callBySipId(session.id);
        }
        if (call) {
            call.setSip(session);
            this.checkAutoAnswer(call);
        }
    }
    checkAutoAnswer(call) {
        if ( /*!document.hidden && */call.autoAnswer) {
            call.answerDelay({
                video: call.params.video,
                screen: call.params.screen,
                disableStun: call.params.disableStun,
            });
        }
    }
    connectToSocket() {
        return new Promise((resolve, reject) => {
            try {
                this.socket = new Socket(this._config.endpoint, this._config.applicationName, this.buildVersion);
                this.socket.connect(this._config.token);
            }
            catch (e) {
                reject(e);
                return;
            }
            this.socket.on('message', this.onMessage.bind(this));
            this.socket.on('close', (code) => {
                if (this.pingTimer) {
                    clearTimeout(this.pingTimer);
                }
                this.log.error('socket close code: ', code);
                if (code !== 1000) {
                    this.emit('disconnected', code, this.lastError);
                    reject(new Error(`close socket code: ${code}`));
                }
                this.eventHandler.off('*');
            });
            this.socket.on('open', () => {
                resolve(null);
                this.emit('connected');
            });
        });
    }
    handleUserStateEvent(event) {
        this.eventHandler.emit(WEBSOCKET_EVENT_USER_STATE, event);
    }
    async handleCallEvents(event) {
        let call;
        event.timestamp = Date.now(); // todo bug
        switch (event.event) {
            case CallActions.Ringing:
                call = new Call(this, event);
                this.emit('call_receive', call);
                this.callStore.set(call.id, call);
                this.checkAutoAnswer(call);
                break;
            case CallActions.Active:
                call = this.callById(event.id);
                if (call) {
                    call.setActive(event);
                }
                break;
            case CallActions.Progress:
                return;
            case CallActions.Bridge:
                call = this.callById(event.id);
                if (call) {
                    call.setBridged(event);
                }
                break;
            case CallActions.Execute:
                call = this.callById(event.id);
                if (call) {
                    call.setExecute(event.data);
                }
                break;
            case CallActions.DTMF:
                call = this.callById(event.id);
                if (call) {
                    call.addDigit(event.data);
                }
                break;
            case CallActions.Eavesdrop:
                call = this.callById(event.id);
                if (call) {
                    call.setEavesdropState(event.data);
                }
                break;
            case CallActions.Voice:
                call = this.callById(event.id);
                if (call) {
                    call.setVoice();
                }
                break;
            case CallActions.Silence:
                call = this.callById(event.id);
                if (call) {
                    call.setSilence();
                }
                break;
            case CallActions.Hold:
                call = this.callById(event.id);
                if (call) {
                    call.setHold(event);
                }
                break;
            case CallActions.Hangup:
                call = this.callById(event.id);
                if (call) {
                    call.setHangup(event);
                    this.emit('call_hangup', call);
                    if (call.notificationHangup) {
                        this.generateTone();
                    }
                }
                break;
            case CallActions.Stats:
                this.emit(WEBSOCKET_EVENT_CALL_MEDIA_METRIC, event.data.rtp);
                break;
            default:
                throw new Error(`Unhandled event ${event.event}`);
        }
        if (call) {
            try {
                this.eventHandler.emit(WEBSOCKET_EVENT_CALL, event.event, call);
            }
            catch (e) {
                // @ts-ignore
                this.log.error(e.message);
            }
            if (this.callDestroyed(call)) {
                this.destroyCall(call);
            }
        }
    }
    generateTone() {
        if (this.toneTimer) {
            return;
        }
        try {
            const context = new AudioContext();
            const oscillator = context.createOscillator(); // Oscillator defaults to sine wave
            oscillator.connect(context.destination);
            // @ts-ignore
            // oscillator.type = "sawtooth";
            oscillator.start();
            // @ts-ignore
            this.toneTimer = setTimeout(() => {
                this.toneTimer = null;
                oscillator.stop();
            }, 500);
        }
        catch (e) {
            this.emit('error', e);
        }
    }
    async handleChatEvents(event) {
        let conversation;
        const timestamp = Date.now(); // todo bug
        switch (event.action) {
            case ChatActions.UserInvite:
                const inv = event.data;
                conversation = new Conversation(this, inv.conversation_id, inv.title, inv.members, inv.messages, inv.variables);
                conversation.setInvite(inv.invite_id, timestamp);
                this.conversationStore.set(conversation.id, conversation);
                break;
            case ChatActions.MessageDeleted:
                const deleted = event.data;
                // fixme
                for (const v of this.allConversations()) {
                    if (!v.closedAt && v.membersId.indexOf(deleted.channel_id) > -1) {
                        conversation = v;
                        break;
                    }
                }
                if (conversation) {
                    conversation.setDeletedMessage(deleted);
                }
                return;
            case ChatActions.Joined:
                const joined = event.data;
                conversation = this.conversationById(joined.member.id);
                if (conversation) {
                    conversation.setAnswered(joined.member.id, timestamp, joined.member);
                }
                break;
            case ChatActions.Message:
                const message = event.data;
                message.timestamp = timestamp;
                // fixme
                for (const v of this.allConversations()) {
                    if (!v.closedAt) {
                        if (v.membersId.indexOf(message.channel_id) > -1) {
                            conversation = v;
                        }
                        else if (!message.channel_id &&
                            v.getConversationId === message.conversation_id) {
                            conversation = v;
                        }
                        if (conversation) {
                            break;
                        }
                    }
                }
                if (conversation) {
                    conversation.newMessage(message);
                }
                break;
            case ChatActions.Close:
                const c = event.data;
                // fixme
                for (const v of this.allConversations()) {
                    if (!v.closedAt && v.membersId.indexOf(c.from_channel_id) > -1) {
                        conversation = v;
                    }
                }
                if (conversation) {
                    conversation.setClosed(timestamp);
                }
                break;
            case ChatActions.Leave:
                const l = event.data;
                conversation = this.conversationById(l.leaved_channel_id);
                if (conversation) {
                    conversation.setLeave(l);
                }
                break;
            case ChatActions.Decline:
                const e = event.data;
                conversation = this.conversationById(e.invite_id);
                if (conversation) {
                    conversation.setDecline(e);
                }
                break;
            default:
        }
        if (conversation) {
            conversation.lastAction = event.action;
            if (conversation.meetingId) {
                for (const c of this.allCall()) {
                    if (c.meetingId === conversation.meetingId) {
                        c.conversation = conversation;
                    }
                    else {
                        this.log.error('not found');
                    }
                }
                return;
            }
            this.eventHandler.emit(WEBSOCKET_EVENT_CHAT, event.action, conversation);
            if (this.conversationDestroyed(conversation)) {
                this.destroyConversation(conversation);
            }
        }
    }
    hasAgentTask(task) {
        if (!this.agent || !task) {
            return false;
        }
        return this.agent.hasTask(task);
    }
    destroyCall(call) {
        // FIXME sync channel & call event
        this.callStore.delete(call.id);
        this.eventHandler.emit(WEBSOCKET_EVENT_CALL, CallActions.Destroy, call);
    }
    fileUrlParams(mime) {
        const params = [`access_token=${this._config.token}`];
        if (mime) {
            const source = mime.replace(/.*;\s?source=([-._A-Za-z0-9/]+).*/, '$1');
            if (source) {
                params.push(`source=${source}`);
            }
        }
        return params;
    }
}
function isJobTask(task) {
    return task.channel === ChannelName.Task;
}
function isDestroyJob(state) {
    return ([
        ChannelState.Missed.toString(),
        ChannelState.Waiting,
        ChannelState.WrapTime,
        JobState.Closed,
    ].indexOf(state) > -1);
}
//# sourceMappingURL=client.js.map