var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { debounce } from 'lodash';
import BaseSessionHandler from './base-session-handler';
import { SessionTypes, SdkErrorTypes, JingleReasons, CommunicationStates, MediaHandling } from '../types/enums';
import { attachAudioMedia, logDeviceChange, createUniqueAudioMediaElement } from '../media/media-utils';
import { requestApi, isSoftphoneJid, createAndEmitSdkError, isPeerConnectionDisconnected } from '../utils';
import { HeadsetChangesQueue } from '../headsets/headset-utils';
import { removeAddressFieldFromConversationUpdate } from '../utils';
import { StatsAggregator } from '../stats-aggregator';
export class SoftphoneSessionHandler extends BaseSessionHandler {
    constructor(sdk, sessionManager) {
        super(sdk, sessionManager);
        this.sessionType = SessionTypes.softphone;
        this.conversations = {};
        this.lastEmittedSdkConversationEvent = {
            activeConversationId: undefined,
            current: [],
            added: [],
            removed: []
        };
        this.debouncedEmitCallError = debounce(this.emitCallError, 500, { leading: true });
    }
    handlePersistentConnectionEvent(event) {
        var _a;
        this.lastPersistentConnectionEvent = event.eventBody;
        if (((_a = event.eventBody.errorInfo) === null || _a === void 0 ? void 0 : _a.code) === 'error.ininedgecontrol.connection.webrtc.endpoint.disconnect') {
            this.log('debug', 'received disconnected persistent connection event');
            return;
        }
        this.log('debug', 'persistent connection event', event.eventBody);
        if (event.eventBody.errorInfo) {
            createAndEmitSdkError.call(this.sdk, SdkErrorTypes.call, event.eventBody.errorInfo.userMessage, event.eventBody);
        }
    }
    listenForPersistentConnectionEvents() {
        return __awaiter(this, void 0, void 0, function* () {
            this.boundPersistentConnectionEventHandler = this.handlePersistentConnectionEvent.bind(this);
            yield this.sdk._streamingConnection.notifications.subscribe(`v2.users.${this.sdk._personDetails.id}.persistentconnection`, this.boundPersistentConnectionEventHandler, true);
        });
    }
    enableHandler() {
        const _super = Object.create(null, {
            enableHandler: { get: () => super.enableHandler }
        });
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.sdk.isGuest) {
                yield this.listenForPersistentConnectionEvents();
            }
            _super.enableHandler.call(this);
        });
    }
    disableHandler() {
        const _super = Object.create(null, {
            disableHandler: { get: () => super.disableHandler }
        });
        return __awaiter(this, void 0, void 0, function* () {
            if (this.boundPersistentConnectionEventHandler) {
                yield this.sdk._streamingConnection.notifications.unsubscribe(`v2.users.${this.sdk._personDetails.id}.persistentconnection`, this.boundPersistentConnectionEventHandler);
                this.boundPersistentConnectionEventHandler = undefined;
            }
            _super.disableHandler.call(this);
        });
    }
    shouldHandleSessionByJid(jid) {
        return isSoftphoneJid(jid);
    }
    handleConversationUpdate(update, sessions) {
        /* we will not have a user call participant if this is not a softphone conversation event */
        const participant = this.getUserParticipantFromConversationEvent(update);
        if (!participant) {
            return this.log('debug', 'user participant not found on the conversation update', update, { skipServer: true });
        }
        const callState = this.getCallStateFromParticipant(participant);
        if (!callState) {
            return this.log('debug', "user participant's call state not found on the conversation update. not processing", { update, participant }, { skipServer: true });
        }
        /* if we get here it means it was a softphone conversation event */
        const lastConversationUpdate = this.conversations[update.id];
        let session;
        /* use our stored session for this conversation */
        if (lastConversationUpdate === null || lastConversationUpdate === void 0 ? void 0 : lastConversationUpdate.session) {
            session = lastConversationUpdate.session;
        }
        /* or, if LineAppearance is 1, use that session */
        else if (!this.sdk.isConcurrentSoftphoneSessionsEnabled()) {
            session = this.activeSession;
        }
        /* lastly, look through our sessions */
        else {
            session = sessions.find(s => s.conversationId === update.id);
        }
        /* if we didn't find a session AND we have persistent connection, we need to do an extra check */
        if (!session && this.sdk.isPersistentConnectionEnabled()) {
            /*
              if we have an active session
              AND it is not currently being used by another conversation
              AND our client handled it.
              this can happen when LA > 1 and persistentConnection is ON
            */
            if (this.hasActiveSession() && !Object.values(this.conversations).find(c => c.session === this.activeSession)) {
                session = this.activeSession;
                this.log('info', 'we have an active session that is not in use by another conversation. using that session', {
                    conversationId: update.id,
                    sessionId: session.id,
                    conversationIdCurrentlyOnSession: session.conversationId
                });
            }
        }
        this.handleSoftphoneConversationUpdate(update, participant, callState, session);
    }
    hasActiveSession() {
        var _a;
        return ((_a = this.activeSession) === null || _a === void 0 ? void 0 : _a.peerConnection.connectionState) === 'connected';
    }
    handlePropose(pendingSession) {
        const _super = Object.create(null, {
            handlePropose: { get: () => super.handlePropose }
        });
        return __awaiter(this, void 0, void 0, function* () {
            const isPrivAnswerAuto = pendingSession.privAnswerMode === 'Auto';
            const eagerConnectionEstablishmentMode = this.sdk._config.eagerPersistentConnectionEstablishment;
            const logInfo = { sessionId: pendingSession === null || pendingSession === void 0 ? void 0 : pendingSession.id, conversationId: pendingSession.conversationId };
            const reducedMediaHandling = this.sdk._mediaHandling === MediaHandling.reducedMediaHeadsets || this.sdk._mediaHandling === MediaHandling.reducedMedia;
            if (reducedMediaHandling) {
                this.log('info', 'received a propose while the SDK is configured for reduced media handling', logInfo);
                if (pendingSession.autoAnswer) {
                    // emit the pendingSession event
                    yield _super.handlePropose.call(this, pendingSession);
                    if (this.sdk._config.disableAutoAnswer) {
                        // It is possible that the consuming client has its own logic for auto-answering calls (e.g. web-dir).
                        this.log('info', 'received an autoAnswer tagged propose but the SDK was configured to not auto-answer, deferring to the consuming client.', logInfo);
                    }
                    else {
                        yield this.proceedWithSession(pendingSession);
                    }
                }
                else {
                    this.log('info', 'media handling is reduced, but this propose is not marked as autoAnswer and will be ignored', logInfo);
                    return;
                }
            }
            else {
                if (isPrivAnswerAuto) {
                    this.log('info', 'received a propose with privAnswerMode=Auto', logInfo);
                    if (eagerConnectionEstablishmentMode === 'none') {
                        this.log('info', 'eagerPersistentConnectionEstablishment is "none" so propose with privAnswerMode=Auto will be ignored', logInfo);
                        return;
                    }
                    else if (eagerConnectionEstablishmentMode === 'auto') {
                        // we don't need to emit a pendingSession event when we auto-answer eager persistent connections
                        return yield this.proceedWithSession(pendingSession);
                    }
                    else {
                        yield _super.handlePropose.call(this, pendingSession);
                    }
                }
                else {
                    // we want to emit the pendingSession event in all other cases
                    yield _super.handlePropose.call(this, pendingSession);
                    // calls will can be marked as auto-answer or priv-answer-mode: Auto, but never both
                    if (pendingSession.autoAnswer) {
                        if (this.sdk._config.disableAutoAnswer) {
                            // It is possible that the consuming client has its own logic for auto-answering calls (e.g. web-dir).
                            this.log('info', 'received an autoAnswer tagged propose but the SDK was configured to not auto-answer, deferring to the consuming client.', logInfo);
                        }
                        else {
                            yield this.proceedWithSession(pendingSession);
                        }
                    }
                }
            }
        });
    }
    getActiveConversations() {
        var _a;
        const currentConversations = ((_a = this.lastEmittedSdkConversationEvent) === null || _a === void 0 ? void 0 : _a.current) || [];
        return currentConversations.map(currentConvo => { var _a; return ({ conversationId: currentConvo.conversationId, sessionId: (_a = currentConvo.session) === null || _a === void 0 ? void 0 : _a.id, sessionType: this.sessionType }); });
    }
    handleSoftphoneConversationUpdate(update, participant, callState, session) {
        var _a;
        const conversationId = update.id;
        const lastConversationUpdate = this.conversations[conversationId];
        const conversationUpdateForLogging = Object.assign({}, lastConversationUpdate);
        delete conversationUpdateForLogging.session;
        if (session) {
            session.pcParticipant = participant;
        }
        this.log('debug', 'about to process conversation event', { sessionId: session === null || session === void 0 ? void 0 : session.id, update, conversationUpdateForLogging, callState });
        /* if we didn't have a previous update and this one is NOT in a pending state, that means we are not responsible for this conversation (another client handled it or we have already emitted the `sessionEnded` for it) */
        if (!lastConversationUpdate && !this.isPendingState(callState)) {
            return this.log('debug', 'received a conversation event for a conversation we are not responsible for. not processing', { update, callState }, { skipServer: true });
        }
        this.checkForCallErrors(update, participant, callState);
        /* update the conversation state */
        const conversationState = {
            mostRecentCallState: callState,
            mostRecentUserParticipant: participant,
            conversationUpdate: update,
            conversationId: conversationId,
            /* this is just a precaution – session for the conversation should be added in `this.acceptSession()` */
            /* this is possible if we have an active PC and do not have an active call (ie. we are going to 1 from 0) */
            session: ((_a = this.conversations[conversationId]) === null || _a === void 0 ? void 0 : _a.session) || (lastConversationUpdate === null || lastConversationUpdate === void 0 ? void 0 : lastConversationUpdate.session) || session
        };
        const previousCallState = this.getUsersCallStateFromConversationEvent(lastConversationUpdate === null || lastConversationUpdate === void 0 ? void 0 : lastConversationUpdate.conversationUpdate);
        this.conversations[conversationId] = conversationState;
        /* if the state didn't change at all, we can skip processing */
        if (!this.diffConversationCallStates(previousCallState, callState)) {
            return this.log('debug', 'conversation update received but state is unchanged. ignoring', {
                conversationId,
                previousCallState,
                callState,
                sessionType: this.sessionType
            });
        }
        /* These next couple of blocks will ensure that the headset device is set to the appropriate state */
        /* It really should only matter for connected calls as ended calls should have the logic already in place in SVH */
        if (this.isConnectedState(callState) && session) {
            if (callState.muted !== (previousCallState === null || previousCallState === void 0 ? void 0 : previousCallState.muted)) {
                HeadsetChangesQueue.queueHeadsetChanges(() => this.sdk.headset.setMute(callState.muted));
            }
            if (callState.held !== (previousCallState === null || previousCallState === void 0 ? void 0 : previousCallState.held)) {
                HeadsetChangesQueue.queueHeadsetChanges(() => this.sdk.headset.setHold(conversationId, callState.held));
            }
        }
        const communicationStateChanged = (previousCallState === null || previousCallState === void 0 ? void 0 : previousCallState.state) !== callState.state;
        let eventToEmit = 'updated';
        const isOutbound = callState.direction === 'outbound';
        /* only check for emitting if the state changes */
        if (communicationStateChanged) {
            /* `pendingSession` – only process these if we have a persistent connection */
            if (this.isPendingState(callState)) {
                // Not always accurate. If inbound auto answer, we don't know about it from convo evt
                // headset actions will be tied to conversation updates rather than session events so we want to react regardless
                if (isOutbound) {
                    HeadsetChangesQueue.queueHeadsetChanges(() => this.sdk.headset.outgoingCall({ conversationId }));
                }
                else {
                    const nrStat = {
                        actionName: 'WebrtcStats',
                        details: {
                            _eventType: 'firstAlertingConversationUpdate',
                            _eventTimestamp: new Date().getTime(),
                            _appId: this.sdk.logger.clientId,
                            _appName: this.sdk.logger.config.appName,
                            _appVersion: this.sdk.VERSION,
                            conversationId,
                            participantId: participant.id,
                        }
                    };
                    this.sdk._streamingConnection._webrtcSessions.proxyNRStat(nrStat);
                    HeadsetChangesQueue.queueHeadsetChanges(() => this.sdk.headset.setRinging({ conversationId, contactName: null }, !!this.lastEmittedSdkConversationEvent.current.length));
                }
                /* only emit `pendingSession` if we already have an active session */
                if (session && session === this.activeSession) {
                    const pendingSession = {
                        id: session.id,
                        sessionId: session.id,
                        autoAnswer: isOutbound,
                        conversationId,
                        sessionType: this.sessionType,
                        originalRoomJid: session.originalRoomJid,
                        fromUserId: session.fromUserId,
                        fromJid: session.peerID,
                        toJid: this.sdk._personDetails.chat.jabberId
                    };
                    this.sessionManager.pendingSessions.push(pendingSession);
                    this.sdk.emit('pendingSession', pendingSession);
                }
                /* we don't want to emit events for these */
                eventToEmit = false;
            }
            else if (this.isConnectedState(callState)) {
                /*
                  if our conversationId is already on the session, that means it started using the standard `session-accept`.
                  if our conversationId is not already on the session AND the previous state wasn't already connected, then
                  we just accepted the session and we need to emit `sessionStarted`
                */
                if (conversationId !== (session === null || session === void 0 ? void 0 : session.conversationId)
                    || (previousCallState && !this.isConnectedState(previousCallState))) {
                    // Conversation update could potentially contain PII in the address field.
                    const updateToLog = removeAddressFieldFromConversationUpdate(update);
                    /* if we are adding a session, but we don't have a session – it means another client took the conversation */
                    if (!session) {
                        this.log('info', 'incoming conversation started, but we do not have a session. assuming it was handled by a different client. ignoring', {
                            ignoredConversation: this.conversations[conversationId],
                            updateToLog
                        });
                        if (this.sdk.headset.orchestrationState === 'hasControls') {
                            this.sdk.headset.resetHeadsetStateForCall(conversationId);
                        }
                        delete this.conversations[conversationId];
                        return;
                    }
                    // if this was an inbound call, the headset needs to move from ringing to answered
                    if (!isOutbound) {
                        HeadsetChangesQueue.queueHeadsetChanges(() => this.sdk.headset.answerIncomingCall(conversationId, false));
                    }
                    /* only emit `sessionStarted` if we have an active session */
                    if (session === this.activeSession) {
                        session.conversationId = conversationId;
                        // we only want to emit a single sessionStarted. We will track those because otherwise we have to make an educated
                        // guess which has the potential to be wrong
                        if (!session._emittedSessionStarteds[conversationId]) {
                            this.sdk.emit('sessionStarted', session);
                            session._emittedSessionStarteds[conversationId] = true;
                        }
                        this.sessionManager.onHandledPendingSession(session.id, conversationId);
                    }
                    eventToEmit = 'added';
                }
            }
            else if (this.isEndedState(callState)) {
                if (this.isPendingState(previousCallState) && !isOutbound) {
                    this.sdk.headset.rejectIncomingCall(conversationId);
                }
                else {
                    this.sdk.headset.endCurrentCall(conversationId, Object.keys(this.conversations).length > 1);
                }
                HeadsetChangesQueue.clearQueue();
                /* we don't want to emit events for (most of) these */
                eventToEmit = false;
                /* we rejected a pendingSession */
                if (this.isPendingState(previousCallState)) {
                    if (session && session === this.activeSession) {
                        this.sessionManager.onCancelPendingSession(session.id, conversationId);
                    }
                    delete this.conversations[conversationId];
                }
                else if (previousCallState) {
                    /*
                      if there is a previous state, that means we are ending this call – otherwise it means we have
                      already ended it (we get `disconnected` and `terminated` events back-to-back for ending calls. We only want to process one of them.)
                    */
                    // STREAM-1591: Unify these events without causing other problems.
                    this.sdk.emit('_sessionEnded', session, { condition: JingleReasons.success });
                    if (session && session === this.activeSession) {
                        session.conversationId = conversationId;
                        this.sdk.emit('sessionEnded', session, { condition: JingleReasons.success });
                    }
                    eventToEmit = 'removed';
                }
            }
        }
        if (eventToEmit) {
            const conversationUpdateForLogging = Object.assign({}, this.conversations[conversationId]);
            delete conversationUpdateForLogging.session;
            this.log('debug', 'about to emit based on conversation event', { eventToEmit, update: conversationUpdateForLogging, previousCallState, callState, sessionId: session === null || session === void 0 ? void 0 : session.id }, { skipServer: true });
            this.emitConversationEvent(eventToEmit, this.conversations[conversationId], session);
        }
    }
    diffConversationCallStates(call1, call2) {
        return !call1
            || !call2
            || call1.state !== call2.state
            || call1.confined !== call2.confined
            || call1.held !== call2.held
            || call1.muted !== call2.muted;
    }
    checkForCallErrors(update, participant, callState) {
        if (callState.errorInfo) {
            if (callState.errorInfo.code === 'error.ininedgecontrol.session.inactive') {
                return;
            }
            this.debouncedEmitCallError(update, participant, callState);
        }
    }
    emitCallError(update, participant, callState) {
        createAndEmitSdkError.call(this.sdk, SdkErrorTypes.call, 'Call error has occurred', { errorInfo: callState.errorInfo, conversationId: update.id });
    }
    emitConversationEvent(event, conversation, session) {
        const current = Object.values(this.conversations);
        const currentEmittedEvent = {
            added: [],
            removed: [],
            current,
            activeConversationId: ''
        };
        if (event === 'added') {
            currentEmittedEvent.added.push(conversation);
        }
        else if (event === 'removed') {
            currentEmittedEvent.removed.push(conversation);
            currentEmittedEvent.current = currentEmittedEvent.current.filter(conversationEvent => conversationEvent.conversationId !== conversation.conversationId);
            delete this.conversations[conversation.conversationId];
        }
        currentEmittedEvent.activeConversationId = this.determineActiveConversationId(session);
        const previousEventForLogging = this.pruneConversationUpdateForLogging(this.lastEmittedSdkConversationEvent);
        const currentEventForLogging = this.pruneConversationUpdateForLogging(currentEmittedEvent);
        this.log('debug', 'emitting `conversationUpdate`', { event, previousEmittedEvent: previousEventForLogging, currentEmittedEvent: currentEventForLogging, sessionId: session === null || session === void 0 ? void 0 : session.id }, { skipServer: true });
        this.lastEmittedSdkConversationEvent = currentEmittedEvent;
        this.sdk.emit('conversationUpdate', currentEmittedEvent);
    }
    pruneConversationUpdateForLogging(update) {
        const replaceSessions = (conversationStates) => {
            return conversationStates.map((conversationState) => {
                var _a;
                const conversationStateCopy = Object.assign(Object.assign({}, conversationState), { sessionId: (_a = conversationState.session) === null || _a === void 0 ? void 0 : _a.id });
                delete conversationStateCopy.session;
                return conversationStateCopy;
            });
        };
        const updateForLogging = Object.assign(Object.assign({}, update), { added: replaceSessions(update.added), removed: replaceSessions(update.removed), current: replaceSessions(update.current) });
        return updateForLogging;
    }
    determineActiveConversationId(session) {
        var _a;
        const conversations = Object.values(this.conversations);
        if (conversations.length === 0) {
            return ((_a = this.activeSession) === null || _a === void 0 ? void 0 : _a.conversationId) || (session === null || session === void 0 ? void 0 : session.conversationId) || '';
        }
        else if (conversations.length === 1) {
            return conversations[0].conversationId;
        }
        /* find the active call */
        const connectedConversations = conversations.filter(c => this.isConnectedState(c.mostRecentCallState));
        /* if there is only one connected call, use it */
        if (connectedConversations.length === 1) {
            return connectedConversations[0].conversationId;
        }
        else if (connectedConversations.length > 1) {
            /* if there are multiple, find the one that isn't held */
            const nonHeldConversations = connectedConversations.find(c => (c === null || c === void 0 ? void 0 : c.mostRecentCallState.held) === false);
            if (nonHeldConversations) {
                return nonHeldConversations.conversationId;
            }
        }
        /* else, just keep the current id on the session */
        return (session === null || session === void 0 ? void 0 : session.conversationId) || ''; // this gets wonky if there are multiple calls all on hold...
    }
    getUsersCallStateFromConversationEvent(update, state) {
        state = state || CommunicationStates.connected;
        return this.getCallStateFromParticipant(this.getUserParticipantFromConversationEvent(update, state));
    }
    getUserParticipantFromConversationEvent(update, state) {
        if (!update) {
            return;
        }
        // Filter out voicemail participants - user ID can appear multiple times with different purposes.
        const participantsForUser = update.participants.filter((p) => p.userId === this.sdk._personDetails.id && p.purpose !== 'voicemail').reverse();
        let participant;
        if (!participantsForUser.length) {
            this.log('warn', 'user not found on conversation as a participant', { conversationId: update.id });
            return;
        }
        /* one participant */
        if (participantsForUser.length === 1) {
            participant = participantsForUser[0];
        }
        /* find user participant with desired call state */
        if (!participant && state) {
            participant = participantsForUser.filter(p => p.calls.find(c => c.state === state))[0];
        }
        /* find user participant with a call */
        if (!participant) {
            participant = participantsForUser.find(p => p.calls.length);
        }
        /* find the most recent user participant */
        if (!participant) {
            participant = participantsForUser[0];
        }
        return participant;
    }
    getCallStateFromParticipant(participant) {
        const calls = participant === null || participant === void 0 ? void 0 : participant.calls;
        const callLen = calls === null || calls === void 0 ? void 0 : calls.length;
        if (!callLen) {
            this.log('debug', 'no call found on participant', { userId: participant === null || participant === void 0 ? void 0 : participant.userId, participantId: participant === null || participant === void 0 ? void 0 : participant.id });
            return;
        }
        /* if we only have one, use it */
        if (callLen === 1) {
            return calls[0];
        }
        const nonEndedCalls = calls.filter(c => !this.isEndedState(c));
        /* if we only have one non-ended call, use it */
        if (nonEndedCalls.length === 1) {
            return nonEndedCalls[0];
        }
        /* else, grab the last one (which should be the most recent) */
        return calls[callLen - 1];
    }
    setCurrentSession(session) {
        const loggy = () => ({
            persistentConnectionSessionId: session.id,
            currentConversationId: session.conversationId,
            isConcurrentSoftphoneSessionsEnabled: this.sdk.isConcurrentSoftphoneSessionsEnabled(),
            isPersistentConnectionEnabled: this.sdk.isPersistentConnectionEnabled(),
            otherActiveSession: this.sdk.sessionManager.getAllActiveSessions()
                .map(s => ({ sessionId: s.id, conversationId: s.conversationId }))
        });
        this.log('info', 'setting current session', loggy());
        this.activeSession = session;
        session.once('terminated', () => {
            this.log('info', 'current session has terminated', loggy());
            const otherActiveSessions = this.sdk.sessionManager.getAllActiveSessions();
            if (this.sdk.isPersistentConnectionEnabled() && !!otherActiveSessions.length) {
                this.log('debug', 'active session ended. Using next available session as persistent connection', {
                    terminatedSession: { sessionId: session.id, conversationId: session.conversationId },
                    selectedSession: { sessionId: otherActiveSessions[0].id, conversationId: otherActiveSessions[0].conversationId }
                });
                this.setCurrentSession(otherActiveSessions[0]);
            }
            else {
                this.activeSession = null;
            }
        });
    }
    handleSessionInit(session) {
        const _super = Object.create(null, {
            handleSessionInit: { get: () => super.handleSessionInit }
        });
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            yield _super.handleSessionInit.call(this, session);
            if (this.sdk._config.reportStatistics) {
                session.statsAggregator = new StatsAggregator(session, this.sdk);
            }
            const acceptParams = { conversationId: session.conversationId };
            // if this is a reinvite, we want to silently replace the old session and mark the old session as replaced
            if (session.reinvite) {
                this.log('info', 'received reinvite offer', Object.assign(Object.assign({}, acceptParams), { sessionId: session.id }));
                const oldSession = this.sessionManager.getAllSessions().find(s => {
                    return s.sessionType === SessionTypes.softphone &&
                        s.conversationId === session.conversationId &&
                        s.id !== session.id;
                });
                if (oldSession) {
                    oldSession.sessionReplacedByReinvite = true;
                    session.reconnectCount = ((_a = oldSession.reconnectCount) !== null && _a !== void 0 ? _a : 0) + 1;
                    const oldSessionInfo = { conversationId: oldSession.conversationId, sessionId: oldSession.id, sessionType: this.sessionType };
                    this.log('info', 'force terminating session that was replaced by reinvite', oldSessionInfo);
                    // if this fails, we don't want it to mess up anything up.
                    this.forceEndSession(oldSession, JingleReasons.alternativeSession)
                        .catch(() => {
                        this.log('warn', 'failed to force terminate the session that was replaced by a reinvite', oldSession);
                    });
                }
            }
            if (this.sdk._config.autoConnectSessions || session.reinvite) {
                return this.acceptSession(session, acceptParams);
            }
        });
    }
    acceptSession(session, params) {
        const _super = Object.create(null, {
            acceptSession: { get: () => super.acceptSession }
        });
        return __awaiter(this, void 0, void 0, function* () {
            const lineAppearance1 = !this.sdk.isConcurrentSoftphoneSessionsEnabled();
            const privAnswerMode = session.privAnswerMode;
            /* If LA>1, hold other active sessions in favor of the latest we're accepting unless its a reinvite
            or we're establishing an eager persistent connection (privAnswer). */
            if (!lineAppearance1 && !session.reinvite && privAnswerMode !== 'Auto') {
                this.holdOtherSessions(session);
            }
            /* if we have an active non-concurrent session, we can drop this accept on the floor */
            if (lineAppearance1 && this.hasActiveSession() && session.id === this.activeSession.id) {
                return this.log('debug', '`acceptSession` called with an active session and LineAppearance of 1. no further action needed. session will automatically accept', {
                    conversationId: session.conversationId,
                    sessionId: session.id
                });
            }
            /* If we are don't already have a session stored, store this one */
            else if ((this.sdk.isPersistentConnectionEnabled() || lineAppearance1) &&
                !this.hasActiveSession()) {
                this.setCurrentSession(session);
            }
            let stream = params.mediaStream || this.sdk._config.defaults.audioStream;
            if (!stream) {
                this.log('info', 'No mediaStream provided, starting media', { conversationId: session.conversationId, sessionId: session.id, sessionType: session.sessionType });
                stream = yield this.sdk.media.startMedia({
                    audio: this.sdk.media.getValidSdkMediaRequestDeviceId(params.audioDeviceId),
                    session
                });
                this.log('info', 'Media started', { conversationId: session.conversationId, sessionId: session.id, sessionType: session.sessionType });
            }
            yield this.addMediaToSession(session, stream);
            session._outboundStream = stream;
            const ids = { conversationId: session.conversationId, sessionId: session.id };
            const volume = this.sdk._config.defaults.audioVolume;
            let element = params.audioElement || this.sdk._config.defaults.audioElement;
            /* if we aren't given an element, then we need to setup our own, unique one (per session), then tear it down on terminate */
            if (!element) {
                element = createUniqueAudioMediaElement();
                session.once('terminated', () => {
                    if (session._outputAudioElement === element) {
                        this.log('debug', 'session ended and was using a unique audio element. removing from DOM', { sessionId: session.id, conversationId: session.conversationId, sessionType: session.sessionType });
                        session._outputAudioElement.parentNode.removeChild(session._outputAudioElement);
                    }
                });
            }
            const receiverWithActiveAudioTrack = session.peerConnection.getReceivers().find((r) => { var _a; return ((_a = r.track) === null || _a === void 0 ? void 0 : _a.kind) === 'audio'; });
            if (receiverWithActiveAudioTrack) {
                const stream = new MediaStream();
                stream.addTrack(receiverWithActiveAudioTrack.track);
                session._outputAudioElement = attachAudioMedia(this.sdk, stream, volume, element, ids);
            }
            else {
                session.on('peerTrackAdded', (_track, stream) => {
                    session._outputAudioElement = attachAudioMedia(this.sdk, stream, volume, element, ids);
                });
            }
            /* make sure we store the session with the conversation state: this is used for LA > 1 _and_ for initial (ie. 1st) sessions */
            if (!this.conversations[session.conversationId]) {
                // we don't want to store the fake conversation when priv-answer-mode=true
                if (session.privAnswerMode !== 'Auto') {
                    this.conversations[session.conversationId] = { session };
                }
            }
            else {
                this.conversations[session.conversationId].session = session;
            }
            yield _super.acceptSession.call(this, session, params);
            logDeviceChange(this.sdk, session, 'sessionStarted');
        });
    }
    proceedWithSession(pendingSession) {
        const _super = Object.create(null, {
            proceedWithSession: { get: () => super.proceedWithSession }
        });
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            if (!this.hasActiveSession() || pendingSession.id !== this.activeSession.id) {
                return _super.proceedWithSession.call(this, pendingSession);
            }
            this.log('info', '`proceedWithSession` called with an active session and/or LA == 1. sending proceed via HTTP request', {
                sessionId: pendingSession.id,
                conversationId: pendingSession.conversationId
            });
            let participant = this.getUserParticipantFromConversationEvent((_a = this.conversations[pendingSession.conversationId]) === null || _a === void 0 ? void 0 : _a.conversationUpdate);
            pendingSession.accepted = true;
            if (!participant) {
                participant = yield this.fetchUserParticipantFromConversationId(pendingSession.conversationId);
            }
            return this.patchPhoneCall(pendingSession.conversationId, participant.id, {
                state: CommunicationStates.connected
            });
        });
    }
    rejectPendingSession(pendingSession) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            let participant = this.getUserParticipantFromConversationEvent((_a = this.conversations[pendingSession.conversationId]) === null || _a === void 0 ? void 0 : _a.conversationUpdate);
            if (!participant) {
                participant = yield this.fetchUserParticipantFromConversationId(pendingSession.conversationId);
            }
            if (participant.purpose === 'user') {
                return this._rejectUcCall(pendingSession.conversationId, participant.id);
            }
            return this.patchPhoneCall(pendingSession.conversationId, participant.id, {
                state: CommunicationStates.disconnected
            });
        });
    }
    _rejectUcCall(conversationId, participantId) {
        return requestApi.call(this.sdk, `/conversations/calls/${conversationId}/participants/${participantId}/replace`, {
            method: 'post',
            data: JSON.stringify({ voicemail: true })
        });
    }
    endSession(conversationId, session, reason) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                this.log("info", "ending session", {
                    sessionId: session.id,
                    sessionConversationId: session.conversationId,
                    sessionType: session.sessionType,
                    reason
                });
                const participant = yield this.getUserParticipantFromConversationId(conversationId);
                const patchPromise = this.patchPhoneCall(conversationId, participant.id, {
                    state: CommunicationStates.disconnected
                });
                const terminatedPromise = new Promise((resolve) => {
                    const listener = (endedSession, reason) => {
                        if (endedSession.id === session.id && endedSession.conversationId === conversationId) {
                            this.log('debug', 'received "sessionEnded" event from session requested by `sdk.endSession()`', { endedSessionId: endedSession.id, conversationId }, { skipServer: true });
                            this.sdk.off('sessionEnded', listener);
                            return resolve(reason);
                        }
                        else {
                            this.log('debug', 'received "sessionEnded" event from session that was NOT requested by `sdk.endSession()`', { endedSessionId: endedSession.id, conversationId, sessionId: session.id }, { skipServer: true });
                        }
                    };
                    this.sdk.on('sessionEnded', listener);
                });
                yield Promise.all([patchPromise, terminatedPromise]);
            }
            catch (error) {
                this.log('error', 'Failed to end session gracefully', { conversationId, sessionId: session.id, error });
                /* if LA > 1, just end the session */
                if (this.sdk.isConcurrentSoftphoneSessionsEnabled()) {
                    return this.endSessionFallback(conversationId, session, reason);
                }
                /* LA == 1, we can only end it if our current call count is 1 (because multiple calls could be using this session) */
                const otherActiveSessions = Object.values(this.conversations)
                    .filter(convo => {
                    return convo.conversationId !== conversationId
                        && (this.isPendingState(convo.mostRecentCallState) || this.isConnectedState(convo.mostRecentCallState));
                })
                    .map(convo => {
                    var _a;
                    return ({
                        conversationId: convo.conversationId,
                        sessionId: (_a = convo.session) === null || _a === void 0 ? void 0 : _a.id
                    });
                });
                if (!otherActiveSessions.length) {
                    this.log('warn', 'session has LineAppearance as 1 but no other active sessions. Will attempt to end session', {
                        conversationId, sessionId: session.id, error
                    });
                    return this.endSessionFallback(conversationId, session, reason);
                }
                throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.session, 'Unable to end the session directly as a fallback because LineAppearance is 1 and there are other active conversations', {
                    failedSession: { conversationId, sessionId: session.id },
                    otherActiveSessions,
                    error
                });
            }
        });
    }
    endSessionFallback(conversationId, session, reason) {
        const _super = Object.create(null, {
            endSession: { get: () => super.endSession }
        });
        return __awaiter(this, void 0, void 0, function* () {
            this.log('info', 'Attempting to end session directly', { sessionId: session.id, conversationId: session.conversationId, sessionType: session.sessionType, reason });
            try {
                yield _super.endSession.call(this, conversationId, session, reason);
            }
            catch (error) {
                throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.session, 'Failed to end session directly', { conversationId: session.conversationId, sessionId: session.id, sessionType: session.sessionType, error });
            }
        });
    }
    fetchUserParticipantFromConversationId(conversationId) {
        return __awaiter(this, void 0, void 0, function* () {
            const body = yield this.fetchConversationStateFromApi(conversationId);
            return this.getUserParticipantFromConversationEvent(body, CommunicationStates.connected);
        });
    }
    getUserParticipantFromConversationId(conversationId) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            let userParticipant = (_a = this.conversations[conversationId]) === null || _a === void 0 ? void 0 : _a.mostRecentUserParticipant;
            if (!userParticipant) {
                userParticipant = yield this.fetchUserParticipantFromConversationId(conversationId);
            }
            if (!userParticipant) {
                throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.generic, 'participant not found for converstionId', { conversationId });
            }
            return userParticipant;
        });
    }
    setAudioMute(session, params) {
        return __awaiter(this, void 0, void 0, function* () {
            const conversationId = params.conversationId || session.conversationId;
            this.log('info', 'setting audio mute state', {
                params,
                sessionId: session.id,
                conversationId,
                sessionType: session.sessionType
            });
            try {
                this.sdk.headset.setMute(params.mute);
                const userParticipant = yield this.getUserParticipantFromConversationId(conversationId);
                return yield this.patchPhoneCall(conversationId, userParticipant.id, { muted: params.mute });
            }
            catch (error) {
                throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.generic, 'Failed to set audioMute', {
                    conversationId: session.conversationId,
                    sessionId: session.id,
                    sessionType: session.sessionType,
                    params,
                    error
                });
            }
        });
    }
    setConversationHeld(session, params) {
        return __awaiter(this, void 0, void 0, function* () {
            if (isPeerConnectionDisconnected(session.peerConnection.connectionState)) {
                this.log('warn', 'peerConnection is disconnected, cancelling attempt to hold', { sessionId: session.id, conversationId: session.conversationId, sessionType: session.sessionType });
                return;
            }
            this.log('info', 'setting conversation "held" state', {
                conversationId: session.conversationId,
                sessionId: session.id,
                sessionType: session.sessionType,
                params
            });
            try {
                // If we're unholding and LA > 1, we need to hold the other active sessions. If PC is on, we need to make sure there are other conversations.
                if (!params.held && this.sdk.isConcurrentSoftphoneSessionsEnabled() && Object.keys(this.conversations).length > 1) {
                    this.holdOtherSessions(session);
                }
                this.sdk.headset.setHold(params.conversationId, params.held);
                const userParticipant = yield this.getUserParticipantFromConversationId(params.conversationId);
                return yield this.patchPhoneCall(params.conversationId, userParticipant.id, { held: params.held });
            }
            catch (error) {
                throw createAndEmitSdkError.call(this.sdk, SdkErrorTypes.call, 'Failed to set held state', {
                    conversationId: session.conversationId,
                    sessionId: session.id,
                    sessionType: session.sessionType,
                    params,
                    error
                });
            }
        });
    }
    holdOtherSessions(currentSession) {
        const sessions = this.sessionManager.getAllActiveSessions();
        /* Hold only softphone sessions and sessions not currently held.
        * This needs to be cross referenced with the active conversations because it's possible to have an idle persistent
        * connection that will attempt to hold the previous ended call
        */
        const otherSessions = sessions.filter(session => {
            const convo = this.conversations[session.conversationId];
            return session.sessionType === SessionTypes.softphone &&
                convo &&
                !this.isConversationHeld(session.conversationId) &&
                !this.isEndedState(convo.mostRecentCallState) &&
                session !== currentSession;
        });
        this.log('debug', 'Received new session or unheld previously held session with LA>1, holding other active sessions.');
        otherSessions.forEach(session => {
            this.setConversationHeld(session, { conversationId: session.conversationId, held: true })
                .catch(err => {
                this.log('warn', 'Failed to hold other session during holdOtherSessions', {
                    sessionId: session.id,
                    conversationId: session.conversationId,
                    error: err
                });
            });
        });
    }
    isConversationHeld(conversationId) {
        var _a, _b;
        return !!((_b = (_a = this.conversations[conversationId]) === null || _a === void 0 ? void 0 : _a.mostRecentCallState) === null || _b === void 0 ? void 0 : _b.held);
    }
    // since softphone sessions will *never* have video, we set the videoDeviceId to undefined so we don't spin up the camera
    updateOutgoingMedia(session, options) {
        const _super = Object.create(null, {
            updateOutgoingMedia: { get: () => super.updateOutgoingMedia }
        });
        return __awaiter(this, void 0, void 0, function* () {
            const newOptions = Object.assign(Object.assign({}, options), { videoDeviceId: undefined });
            return _super.updateOutgoingMedia.call(this, session, newOptions);
        });
    }
    startSession(params) {
        return __awaiter(this, void 0, void 0, function* () {
            this.log('info', 'Creating softphone call from SDK', { conversationIds: params.conversationIds, sessionType: this.sessionType });
            const response = yield requestApi.call(this.sdk, `/conversations/calls`, {
                method: 'post',
                data: JSON.stringify(params)
            });
            return { id: response.data.id, selfUri: response.data.selfUri };
        });
    }
    patchPhoneCall(conversationId, participantId, body) {
        return __awaiter(this, void 0, void 0, function* () {
            return requestApi.call(this.sdk, `/conversations/calls/${conversationId}/participants/${participantId}`, {
                method: 'patch',
                data: JSON.stringify(body)
            });
        });
    }
    isPendingState(call) {
        return (call === null || call === void 0 ? void 0 : call.state) === CommunicationStates.alerting || (call === null || call === void 0 ? void 0 : call.state) === CommunicationStates.contacting;
    }
    isConnectedState(call) {
        return (call === null || call === void 0 ? void 0 : call.state) === CommunicationStates.dialing || (call === null || call === void 0 ? void 0 : call.state) === CommunicationStates.connected;
    }
    isEndedState(call) {
        return (call === null || call === void 0 ? void 0 : call.state) === CommunicationStates.disconnected || (call === null || call === void 0 ? void 0 : call.state) === CommunicationStates.terminated;
    }
}
export default SoftphoneSessionHandler;
//# sourceMappingURL=softphone-session-handler.js.map