import { METHOD_START_MESSAGE } from '../../common/constants';
import { emitFinalFailure, handleRegistrationErrors, uploadLogs } from '../../common';
import webWorkerStr from './webWorkerStr';
import { METRIC_EVENT, METRIC_TYPE, REG_ACTION, } from '../../Metrics/types';
import { getMetricManager } from '../../Metrics';
import { getCallManager } from '../calling';
import log from '../../Logger';
import SDKConnector from '../../SDKConnector';
import { ALLOWED_SERVICES, HTTP_METHODS, RegistrationStatus, ServiceIndicator, WorkerMessageType, } from '../../common/types';
import { CALLING_USER_AGENT, CISCO_DEVICE_URL, DEVICES_ENDPOINT_RESOURCE, SPARK_USER_AGENT, BASE_REG_RETRY_TIMER_VAL_IN_SEC, BASE_REG_TIMER_MFACTOR, SEC_TO_MSEC_MFACTOR, REG_RANDOM_T_FACTOR_UPPER_LIMIT, REG_TRY_BACKUP_TIMER_VAL_IN_SEC, MINUTES_TO_SEC_MFACTOR, REG_429_RETRY_UTIL, REG_FAILBACK_429_MAX_RETRIES, FAILBACK_UTIL, REGISTRATION_FILE, DEFAULT_REHOMING_INTERVAL_MIN, DEFAULT_REHOMING_INTERVAL_MAX, DEFAULT_KEEPALIVE_INTERVAL, FAILOVER_UTIL, REGISTER_UTIL, RETRY_TIMER_UPPER_LIMIT, KEEPALIVE_UTIL, REGISTRATION_UTIL, METHODS, URL_ENDPOINT, RECONNECT_ON_FAILURE_UTIL, FAILOVER_CACHE_PREFIX, } from '../constants';
import { LINE_EVENTS } from '../line/types';
import { APIRequest } from '../utils/request';
export class Registration {
    sdkConnector;
    webex;
    userId = '';
    serviceData;
    failback429RetryAttempts;
    registrationStatus;
    failbackTimer;
    activeMobiusUrl;
    rehomingIntervalMin;
    rehomingIntervalMax;
    mutex;
    metricManager;
    lineEmitter;
    callManager;
    deviceInfo = {};
    primaryMobiusUris;
    backupMobiusUris;
    registerRetry = false;
    reconnectPending = false;
    jwe;
    isCCFlow = false;
    failoverImmediately = false;
    retryAfter;
    scheduled429Retry = false;
    webWorker;
    apiRequest;
    constructor(webex, serviceData, mutex, lineEmitter, logLevel, jwe) {
        this.jwe = jwe;
        this.sdkConnector = SDKConnector;
        this.serviceData = serviceData;
        this.isCCFlow = serviceData.indicator === ServiceIndicator.CONTACT_CENTER;
        if (!this.sdkConnector.getWebex()) {
            SDKConnector.setWebex(webex);
        }
        this.webex = this.sdkConnector.getWebex();
        this.userId = this.webex.internal.device.userId;
        this.registrationStatus = RegistrationStatus.IDLE;
        this.failback429RetryAttempts = 0;
        log.setLogger(logLevel, REGISTRATION_FILE);
        this.rehomingIntervalMin = DEFAULT_REHOMING_INTERVAL_MIN;
        this.rehomingIntervalMax = DEFAULT_REHOMING_INTERVAL_MAX;
        this.mutex = mutex;
        this.callManager = getCallManager(this.webex, serviceData.indicator);
        this.metricManager = getMetricManager(this.webex, serviceData.indicator);
        this.lineEmitter = lineEmitter;
        this.primaryMobiusUris = [];
        this.backupMobiusUris = [];
        this.apiRequest = APIRequest.getInstance({ webex: this.webex });
    }
    getServerType(url) {
        return ((this.primaryMobiusUris.includes(url) && 'PRIMARY') ||
            (this.backupMobiusUris?.includes(url) && 'BACKUP') ||
            'UNKNOWN');
    }
    getFailoverCacheKey() {
        return `${FAILOVER_CACHE_PREFIX}.${this.userId || 'unknown'}`;
    }
    saveFailoverState(failoverState) {
        try {
            localStorage.setItem(this.getFailoverCacheKey(), JSON.stringify(failoverState));
        }
        catch (error) {
            log.warn(`Storing failover state in cache failed with error: ${String(error)}`, {
                file: REGISTRATION_FILE,
                method: 'saveFailoverState',
            });
        }
    }
    clearFailoverState() {
        try {
            localStorage.removeItem(this.getFailoverCacheKey());
        }
        catch {
            log.warn('Clearing failover state from localStorage failed', {
                file: REGISTRATION_FILE,
                method: 'clearFailoverState',
            });
        }
    }
    async resumeFailover() {
        try {
            const cachedState = localStorage.getItem(this.getFailoverCacheKey());
            const failoverState = cachedState
                ? JSON.parse(cachedState)
                : undefined;
            if (failoverState && !this.isDeviceRegistered()) {
                const currentTime = Math.floor(Date.now() / 1000);
                const newElapsed = failoverState.timeElapsed + (currentTime - failoverState.retryScheduledTime);
                this.clearFailoverState();
                await this.startFailoverTimer(failoverState.attempt, newElapsed);
                return true;
            }
        }
        catch (error) {
            log.warn(`No failover state found in cache`, {
                file: REGISTRATION_FILE,
                method: 'triggerRegistration',
            });
        }
        return false;
    }
    getActiveMobiusUrl() {
        return this.activeMobiusUrl;
    }
    setActiveMobiusUrl(url) {
        log.info(`${METHOD_START_MESSAGE} with ${url}`, {
            method: METHODS.UPDATE_ACTIVE_MOBIUS,
            file: REGISTRATION_FILE,
        });
        this.activeMobiusUrl = url;
        this.callManager.updateActiveMobius(url);
    }
    setDeviceInfo(devicesInfo) {
        const [device] = devicesInfo.devices;
        this.deviceInfo = {
            userId: devicesInfo.userId,
            device,
            devices: devicesInfo.devices,
        };
    }
    setMobiusServers(primaryMobiusUris, backupMobiusUris) {
        log.log(METHOD_START_MESSAGE, { method: METHODS.SET_MOBIUS_SERVERS, file: REGISTRATION_FILE });
        this.primaryMobiusUris = primaryMobiusUris;
        this.backupMobiusUris = backupMobiusUris;
    }
    async deleteRegistration(url, deviceId, deviceUrl) {
        let response;
        const requestObj = {
            uri: `${url}${DEVICES_ENDPOINT_RESOURCE}/${deviceId}`,
            method: HTTP_METHODS.DELETE,
            headers: {
                [CISCO_DEVICE_URL]: deviceUrl,
                [SPARK_USER_AGENT]: CALLING_USER_AGENT,
            },
            service: ALLOWED_SERVICES.MOBIUS,
        };
        if (this.apiRequest.isSocketEnabled()) {
            requestObj.body = {
                deviceId,
            };
        }
        try {
            response = await this.apiRequest.makeRequest(requestObj);
        }
        catch (error) {
            log.warn(`Delete failed with Mobius: ${JSON.stringify(error)}`, {
                file: REGISTRATION_FILE,
                method: METHODS.DELETE_REGISTRATION,
            });
            await uploadLogs();
        }
        this.setStatus(RegistrationStatus.INACTIVE);
        this.lineEmitter(LINE_EVENTS.UNREGISTERED);
        return response;
    }
    async postRegistration(url) {
        const deviceInfo = {
            userId: this.userId,
            clientDeviceUri: this.webex.internal.device.url,
            serviceData: this.jwe ? { ...this.serviceData, jwe: this.jwe } : this.serviceData,
        };
        return this.apiRequest.makeRequest({
            uri: `${url}device`,
            method: HTTP_METHODS.POST,
            headers: {
                [CISCO_DEVICE_URL]: deviceInfo.clientDeviceUri,
                [SPARK_USER_AGENT]: CALLING_USER_AGENT,
            },
            body: deviceInfo,
            service: ALLOWED_SERVICES.MOBIUS,
        });
    }
    async restorePreviousRegistration(caller) {
        const loggerContext = {
            file: REGISTRATION_FILE,
            method: METHODS.RESTORE_PREVIOUS_REGISTRATION,
        };
        log.info(`${METHOD_START_MESSAGE} - caller: ${caller}, activeMobiusUrl: ${this.activeMobiusUrl}`, loggerContext);
        let abort = false;
        if (this.apiRequest.isSocketEnabled() &&
            `${this.apiRequest.getConnectedWebSocketUrl()}/` !== this.activeMobiusUrl) {
            log.info(`Disconnecting from Mobius socket to restore previous registration.`, {
                file: REGISTRATION_FILE,
                method: 'restorePreviousRegistration',
            });
            await this.apiRequest.disconnectFromMobiusSocket({
                code: 3050,
                reason: 'done (permanent)',
            });
        }
        if (this.activeMobiusUrl) {
            abort = await this.attemptRegistrationWithServers(caller, [this.activeMobiusUrl]);
            if (this.retryAfter) {
                if (this.retryAfter < RETRY_TIMER_UPPER_LIMIT) {
                    setTimeout(async () => {
                        await this.restartRegistration(caller);
                    }, this.retryAfter * 1000);
                }
                else if (this.primaryMobiusUris.includes(this.activeMobiusUrl) &&
                    this.backupMobiusUris.length > 0) {
                    abort = await this.attemptRegistrationWithServers(caller, this.backupMobiusUris);
                }
                else {
                    await this.restartRegistration(caller);
                }
                this.retryAfter = undefined;
                log.log(`restorePreviousRegistration completed via 429 retry-after path - caller: ${caller}`, loggerContext);
                return true;
            }
        }
        log.log(`restorePreviousRegistration completed - caller: ${caller}, deviceRegistered: ${this.isDeviceRegistered()}, abort: ${abort}`, loggerContext);
        return abort;
    }
    async handle404KeepaliveFailure(caller) {
        const loggerContext = {
            file: REGISTRATION_FILE,
            method: METHODS.HANDLE_404_KEEPALIVE_FAILURE,
        };
        log.info(`${METHOD_START_MESSAGE} - caller: ${caller}`, loggerContext);
        if (caller === KEEPALIVE_UTIL) {
            const abort = await this.attemptRegistrationWithServers(caller);
            if (!abort && !this.isDeviceRegistered()) {
                log.warn('Keepalive 404 recovery: re-registration did not complete, starting failover timer', loggerContext);
                await this.startFailoverTimer();
            }
            else {
                log.log(`Keepalive 404 recovery handled - deviceRegistered: ${this.isDeviceRegistered()}, abort: ${abort}`, loggerContext);
            }
        }
    }
    async handle429Retry(retryAfter, caller) {
        if (caller === FAILBACK_UTIL) {
            if (this.failback429RetryAttempts >= REG_FAILBACK_429_MAX_RETRIES) {
                return;
            }
            this.clearFailbackTimer();
            this.failback429RetryAttempts += 1;
            log.log(`Received 429 while rehoming, 429 retry count : ${this.failback429RetryAttempts}`, {
                file: REGISTRATION_FILE,
                method: REG_429_RETRY_UTIL,
            });
            const interval = this.getRegRetryInterval(this.failback429RetryAttempts);
            this.startFailbackTimer(interval);
            this.scheduled429Retry = true;
            const abort = await this.restorePreviousRegistration(REG_429_RETRY_UTIL);
            if (!abort && !this.isDeviceRegistered()) {
                await this.restartRegistration(REG_429_RETRY_UTIL);
            }
        }
        else if (caller === KEEPALIVE_UTIL) {
            this.clearKeepaliveTimer();
            setTimeout(async () => {
                log.log(`Resuming keepalive after ${retryAfter} seconds`, {
                    file: REGISTRATION_FILE,
                    method: REG_429_RETRY_UTIL,
                });
                await this.startKeepaliveTimer(this.deviceInfo.device?.uri, this.deviceInfo.keepaliveInterval, 'UNKNOWN');
            }, (retryAfter - Number(this.deviceInfo.keepaliveInterval || 0)) * 1000);
        }
        else {
            this.retryAfter = retryAfter;
        }
    }
    getRegRetryInterval(attempt = 1) {
        return (BASE_REG_RETRY_TIMER_VAL_IN_SEC +
            BASE_REG_TIMER_MFACTOR ** attempt +
            Math.floor((Math.random() * (REG_RANDOM_T_FACTOR_UPPER_LIMIT - SEC_TO_MSEC_MFACTOR + 1) +
                SEC_TO_MSEC_MFACTOR) /
                SEC_TO_MSEC_MFACTOR));
    }
    async startFailoverTimer(attempt = 1, timeElapsed = 0) {
        const loggerContext = {
            file: REGISTRATION_FILE,
            method: FAILOVER_UTIL,
        };
        let interval = this.getRegRetryInterval(attempt);
        const TIMER_THRESHOLD = REG_TRY_BACKUP_TIMER_VAL_IN_SEC;
        if (timeElapsed + interval > TIMER_THRESHOLD) {
            const excessVal = timeElapsed + interval - TIMER_THRESHOLD;
            interval -= excessVal;
        }
        if (this.retryAfter != null && interval < this.retryAfter) {
            this.failoverImmediately = this.retryAfter + timeElapsed > TIMER_THRESHOLD;
        }
        let abort;
        if (interval > BASE_REG_RETRY_TIMER_VAL_IN_SEC && !this.failoverImmediately) {
            const scheduledTime = Math.floor(Date.now() / 1000);
            if (this.retryAfter != null) {
                interval = Math.max(interval, this.retryAfter);
            }
            this.saveFailoverState({
                attempt,
                timeElapsed,
                retryScheduledTime: scheduledTime,
                serverType: 'primary',
            });
            setTimeout(async () => {
                await this.mutex.runExclusive(async () => {
                    abort = await this.attemptRegistrationWithServers(FAILOVER_UTIL);
                    const currentTime = Math.floor(Date.now() / 1000);
                    if (!abort && !this.isDeviceRegistered()) {
                        await this.startFailoverTimer(attempt + 1, timeElapsed + (currentTime - scheduledTime));
                    }
                });
            }, interval * SEC_TO_MSEC_MFACTOR);
            log.log(`Scheduled retry with primary in ${interval} seconds, number of attempts : ${attempt}`, loggerContext);
        }
        else if (this.backupMobiusUris.length) {
            if (this.apiRequest.isSocketEnabled()) {
                log.info('Disconnecting from primary Mobius socket for failover to backup servers', loggerContext);
                await this.apiRequest.disconnectFromMobiusSocket({
                    code: 3050,
                    reason: 'done (permanent)',
                });
                log.log('Mobius socket disconnect complete prior to backup failover', loggerContext);
            }
            this.saveFailoverState({
                attempt,
                timeElapsed,
                retryScheduledTime: Math.floor(Date.now() / 1000),
                serverType: 'backup',
            });
            log.info('Failing over to backup servers.', loggerContext);
            this.failoverImmediately = false;
            abort = await this.attemptRegistrationWithServers(FAILOVER_UTIL, this.backupMobiusUris);
            if (!abort && !this.isDeviceRegistered()) {
                interval = this.getRegRetryInterval();
                if (this.retryAfter != null && this.retryAfter < RETRY_TIMER_UPPER_LIMIT) {
                    interval = interval < this.retryAfter ? this.retryAfter : interval;
                }
                setTimeout(async () => {
                    await this.mutex.runExclusive(async () => {
                        abort = await this.attemptRegistrationWithServers(FAILOVER_UTIL, this.backupMobiusUris);
                        if (!abort && !this.isDeviceRegistered()) {
                            await uploadLogs();
                            emitFinalFailure((clientError) => {
                                this.lineEmitter(LINE_EVENTS.ERROR, undefined, clientError);
                            }, loggerContext);
                        }
                    });
                }, interval * SEC_TO_MSEC_MFACTOR);
                log.log(`Scheduled retry with backup servers in ${interval} seconds.`, loggerContext);
            }
        }
        else {
            await uploadLogs();
            emitFinalFailure((clientError) => {
                this.lineEmitter(LINE_EVENTS.ERROR, undefined, clientError);
            }, loggerContext, interval < 0 ? 'Timer threshold exceeded during failover' : undefined);
        }
    }
    clearFailbackTimer() {
        if (this.failbackTimer) {
            clearTimeout(this.failbackTimer);
            this.failbackTimer = undefined;
        }
    }
    async postKeepAlive(deviceUrl, url) {
        return this.apiRequest.makeRequest({
            uri: `${url}/status`,
            method: HTTP_METHODS.POST,
            headers: {
                [CISCO_DEVICE_URL]: deviceUrl,
                [SPARK_USER_AGENT]: CALLING_USER_AGENT,
            },
            body: {
                deviceId: this.deviceInfo.device?.deviceId,
            },
            service: ALLOWED_SERVICES.MOBIUS,
        });
    }
    async isPrimaryActive() {
        let status;
        for (const mobiusUrl of this.primaryMobiusUris) {
            try {
                const baseUri = mobiusUrl.replace(URL_ENDPOINT, '/').replace('wss://', 'https://');
                const response = await this.webex.request({
                    uri: `${baseUri}ping`,
                    method: HTTP_METHODS.GET,
                    headers: {
                        [CISCO_DEVICE_URL]: this.webex.internal.device.url,
                        [SPARK_USER_AGENT]: CALLING_USER_AGENT,
                    },
                    service: ALLOWED_SERVICES.MOBIUS,
                });
                const { statusCode } = response;
                if (statusCode === 200) {
                    log.info(`Ping successful for primary Mobius: ${mobiusUrl}`, {
                        file: REGISTRATION_FILE,
                        method: FAILBACK_UTIL,
                    });
                    status = 'up';
                    break;
                }
            }
            catch (error) {
                log.warn(`Ping failed for primary Mobius: ${mobiusUrl} with error: ${JSON.stringify(error)}`, {
                    file: REGISTRATION_FILE,
                    method: FAILBACK_UTIL,
                });
                status = 'down';
            }
        }
        return status === 'up';
    }
    isFailbackRequired() {
        return this.isDeviceRegistered() && this.primaryMobiusUris.indexOf(this.activeMobiusUrl) === -1;
    }
    getFailbackInterval() {
        return Math.floor(Math.random() * (this.rehomingIntervalMax - this.rehomingIntervalMin + 1) +
            this.rehomingIntervalMin);
    }
    initiateFailback() {
        const loggerContext = {
            file: REGISTRATION_FILE,
            method: METHODS.INITIATE_FAILBACK,
        };
        if (this.isFailbackRequired()) {
            if (!this.failbackTimer) {
                this.failback429RetryAttempts = 0;
                const intervalInMinutes = this.getFailbackInterval();
                log.info(`Scheduling failback to primary - intervalMinutes: ${intervalInMinutes}`, loggerContext);
                this.startFailbackTimer(intervalInMinutes * MINUTES_TO_SEC_MFACTOR);
            }
            else {
                log.info('Failback timer already scheduled, skipping', loggerContext);
            }
        }
        else {
            log.info('Failback not required, clearing any pending failback timer', loggerContext);
            this.failback429RetryAttempts = 0;
            this.clearFailbackTimer();
        }
    }
    startFailbackTimer(intervalInSeconds) {
        this.failbackTimer = setTimeout(async () => this.executeFailback(), intervalInSeconds * SEC_TO_MSEC_MFACTOR);
        log.log(`Failback scheduled after ${intervalInSeconds} seconds.`, {
            file: REGISTRATION_FILE,
            method: this.startFailbackTimer.name,
        });
    }
    async executeFailback() {
        const loggerContext = {
            file: REGISTRATION_FILE,
            method: METHODS.EXECUTE_FAILBACK,
        };
        log.info(METHOD_START_MESSAGE, loggerContext);
        await this.mutex.runExclusive(async () => {
            if (this.isFailbackRequired()) {
                const primaryServerStatus = await this.isPrimaryActive();
                if (Object.keys(this.callManager.getActiveCalls()).length === 0 && primaryServerStatus) {
                    log.info(`Attempting failback to primary.`, loggerContext);
                    await this.deregister();
                    if (this.apiRequest.isSocketEnabled()) {
                        log.info('Disconnecting from backup Mobius socket for failback to primary', loggerContext);
                        await this.apiRequest.disconnectFromMobiusSocket({
                            code: 3050,
                            reason: 'done (permanent)',
                        });
                        log.log('Mobius socket disconnect complete prior to primary failback', loggerContext);
                    }
                    const abort = await this.attemptRegistrationWithServers(FAILBACK_UTIL);
                    if (this.scheduled429Retry || abort || this.isDeviceRegistered()) {
                        return;
                    }
                    const abortNew = await this.restorePreviousRegistration(FAILBACK_UTIL);
                    if (abortNew) {
                        this.clearFailbackTimer();
                        return;
                    }
                    if (!this.isDeviceRegistered()) {
                        await this.restartRegistration(loggerContext.method);
                    }
                    else {
                        this.failbackTimer = undefined;
                        this.initiateFailback();
                    }
                }
                else {
                    log.info('Active calls present or primary Mobius is down, deferring failback to next cycle.', loggerContext);
                    this.failbackTimer = undefined;
                    this.initiateFailback();
                }
            }
        });
    }
    setIntervalValues(deviceInfo) {
        if (this.primaryMobiusUris.indexOf(this.activeMobiusUrl) !== -1) {
            this.rehomingIntervalMin = deviceInfo?.rehomingIntervalMin
                ? deviceInfo.rehomingIntervalMin
                : DEFAULT_REHOMING_INTERVAL_MIN;
            this.rehomingIntervalMax = deviceInfo?.rehomingIntervalMax
                ? deviceInfo.rehomingIntervalMax
                : DEFAULT_REHOMING_INTERVAL_MAX;
        }
    }
    getDeviceInfo() {
        return this.deviceInfo;
    }
    isDeviceRegistered() {
        return this.registrationStatus === RegistrationStatus.ACTIVE;
    }
    getStatus() {
        return this.registrationStatus;
    }
    setStatus(value) {
        this.registrationStatus = value;
    }
    async restartRegistration(caller) {
        const loggerContext = {
            file: REGISTRATION_FILE,
            method: METHODS.RESTART_REGISTRATION,
        };
        log.info(`${METHOD_START_MESSAGE} - caller: ${caller}`, loggerContext);
        this.clearFailbackTimer();
        this.failback429RetryAttempts = 0;
        const abort = await this.attemptRegistrationWithServers(caller, this.primaryMobiusUris);
        if (!abort && !this.isDeviceRegistered()) {
            await this.startFailoverTimer();
        }
        log.log(`restartRegistration completed - caller: ${caller}, deviceRegistered: ${this.isDeviceRegistered()}, abort: ${abort}`, loggerContext);
    }
    async handleConnectionRestoration(retry) {
        log.info(METHOD_START_MESSAGE, {
            method: METHODS.HANDLE_CONNECTION_RESTORATION,
            file: REGISTRATION_FILE,
        });
        await this.mutex.runExclusive(async () => {
            if (retry) {
                log.log('Network is up again, re-registering with Webex Calling if needed', {
                    file: REGISTRATION_FILE,
                    method: METHODS.HANDLE_CONNECTION_RESTORATION,
                });
                this.clearKeepaliveTimer();
                if (this.isDeviceRegistered()) {
                    await this.deregister();
                }
                if (this.activeMobiusUrl) {
                    const abort = await this.restorePreviousRegistration(METHODS.HANDLE_CONNECTION_RESTORATION);
                    if (!abort && !this.isDeviceRegistered()) {
                        await this.restartRegistration(METHODS.HANDLE_CONNECTION_RESTORATION);
                    }
                }
                retry = false;
            }
        });
        return retry;
    }
    restoreRegistrationCallBack() {
        return async (restoreData, caller) => {
            const logContext = { file: REGISTRATION_FILE, method: caller };
            if (!this.isRegRetry()) {
                log.info('Registration restoration in progress.', logContext);
                const restore = this.getExistingDevice(restoreData);
                if (restore) {
                    this.setRegRetry(true);
                    await this.deregister();
                    const finalError = await this.restorePreviousRegistration(caller);
                    this.setRegRetry(false);
                    if (this.isDeviceRegistered()) {
                        log.info('Registration restored successfully.', logContext);
                    }
                    return finalError;
                }
                this.lineEmitter(LINE_EVENTS.UNREGISTERED);
            }
            else {
                this.lineEmitter(LINE_EVENTS.UNREGISTERED);
            }
            return false;
        };
    }
    async triggerRegistration() {
        const loggerContext = {
            file: REGISTRATION_FILE,
            method: METHODS.TRIGGER_REGISTRATION,
        };
        log.info(METHOD_START_MESSAGE, loggerContext);
        if (await this.resumeFailover()) {
            log.info('Registration trigger handled by resumeFailover path', loggerContext);
            return;
        }
        if (this.primaryMobiusUris.length > 0) {
            const abort = await this.attemptRegistrationWithServers(REGISTRATION_UTIL, this.primaryMobiusUris);
            if (!this.isDeviceRegistered() && !abort) {
                await this.startFailoverTimer();
            }
            log.log(`triggerRegistration completed - deviceRegistered: ${this.isDeviceRegistered()}, abort: ${abort}`, loggerContext);
        }
        else {
            log.warn('triggerRegistration skipped: no primary Mobius URIs available', loggerContext);
        }
    }
    async attemptRegistrationWithServers(caller, servers = this.primaryMobiusUris) {
        const loggerContext = {
            file: REGISTRATION_FILE,
            method: REGISTER_UTIL,
        };
        let abort = false;
        this.retryAfter = undefined;
        let connectedWebSocketUrl;
        if (this.failoverImmediately) {
            return abort;
        }
        if (this.isDeviceRegistered()) {
            log.info(`[${caller}] : Device already registered with : ${this.activeMobiusUrl}`, loggerContext);
            return abort;
        }
        for (const url of servers) {
            const serverType = this.getServerType(url);
            try {
                abort = false;
                this.registrationStatus = RegistrationStatus.INACTIVE;
                this.lineEmitter(LINE_EVENTS.CONNECTING);
                log.info(`[${caller}] : Mobius url to contact: ${url}`, loggerContext);
                if (this.apiRequest.isSocketEnabled()) {
                    const wssNormalizedUrl = url.endsWith('/') ? url.slice(0, -1) : url;
                    log.info(`[${caller}] : Connecting to Mobius WebSocket - serverType: ${serverType}, wssUrl: ${wssNormalizedUrl}`, loggerContext);
                    connectedWebSocketUrl = await this.apiRequest.connectToMobiusSocket(wssNormalizedUrl);
                    connectedWebSocketUrl = connectedWebSocketUrl ? `${connectedWebSocketUrl}/` : undefined;
                    log.log(`[${caller}] : Mobius WebSocket connected - serverType: ${serverType}, connectedWebSocketUrl: ${connectedWebSocketUrl}`, loggerContext);
                }
                const resp = await this.postRegistration(url);
                this.clearFailoverState();
                this.deviceInfo = resp.body;
                this.registrationStatus = RegistrationStatus.ACTIVE;
                this.setActiveMobiusUrl(connectedWebSocketUrl || url);
                this.lineEmitter(LINE_EVENTS.REGISTERED, resp.body);
                log.log(`Registration successful for deviceId: ${this.deviceInfo.device?.deviceId} userId: ${this.userId} responseTrackingId: ${resp.headers?.trackingid}`, loggerContext);
                this.setIntervalValues(this.deviceInfo);
                this.metricManager.setDeviceInfo(this.deviceInfo);
                this.metricManager.submitRegistrationMetric(METRIC_EVENT.REGISTRATION, REG_ACTION.REGISTER, METRIC_TYPE.BEHAVIORAL, caller, serverType, resp.headers?.trackingid ?? '', undefined, undefined);
                this.startKeepaliveTimer(this.deviceInfo.device?.uri, this.deviceInfo.keepaliveInterval, serverType);
                this.initiateFailback();
                break;
            }
            catch (err) {
                const body = err;
                const { finalError, shouldDisconnect } = await handleRegistrationErrors(body, (clientError, isFinalError) => {
                    if (isFinalError) {
                        this.lineEmitter(LINE_EVENTS.ERROR, undefined, clientError);
                    }
                    else {
                        this.lineEmitter(LINE_EVENTS.UNREGISTERED);
                    }
                    this.metricManager.submitRegistrationMetric(METRIC_EVENT.REGISTRATION_ERROR, REG_ACTION.REGISTER, METRIC_TYPE.BEHAVIORAL, caller, serverType, body.headers?.trackingid ?? '', undefined, clientError);
                }, { method: caller, file: REGISTRATION_FILE }, (retryAfter, retryCaller) => this.handle429Retry(retryAfter, retryCaller), this.restoreRegistrationCallBack(), servers.length);
                abort = finalError;
                if (this.registrationStatus === RegistrationStatus.ACTIVE) {
                    log.info(`[${caller}] : Device is already restored, active mobius url: ${this.activeMobiusUrl}`, loggerContext);
                    break;
                }
                if (shouldDisconnect && this.apiRequest.isSocketEnabled()) {
                    connectedWebSocketUrl = undefined;
                    log.info(`[${caller}] : Tearing down Mobius WebSocket after registration error - serverType: ${serverType}`, loggerContext);
                    await this.apiRequest.disconnectFromMobiusSocket({
                        code: 3050,
                        reason: 'done (permanent)',
                    });
                    log.log(`[${caller}] : Mobius WebSocket disconnect complete after registration error - serverType: ${serverType}`, loggerContext);
                }
                if (abort) {
                    this.setStatus(RegistrationStatus.INACTIVE);
                    await uploadLogs();
                    break;
                }
            }
        }
        return abort;
    }
    async startKeepaliveTimer(url, interval, serverType) {
        this.clearKeepaliveTimer();
        const RETRY_COUNT_THRESHOLD = this.isCCFlow ? 4 : 5;
        await this.mutex.runExclusive(async () => {
            if (this.isDeviceRegistered()) {
                if (!this.webWorker) {
                    const blob = new Blob([webWorkerStr], { type: 'application/javascript' });
                    const blobUrl = URL.createObjectURL(blob);
                    this.webWorker = new Worker(blobUrl);
                    URL.revokeObjectURL(blobUrl);
                    this.webWorker.postMessage({
                        type: WorkerMessageType.START_KEEPALIVE,
                        interval,
                        retryCountThreshold: RETRY_COUNT_THRESHOLD,
                    });
                    this.webWorker.onmessage = async (event) => {
                        const logContext = {
                            file: REGISTRATION_FILE,
                            method: KEEPALIVE_UTIL,
                        };
                        if (event.data.type === WorkerMessageType.SEND_KEEPALIVE) {
                            try {
                                const res = await this.postKeepAlive(String(this.webex.internal.device.url), url);
                                this.webWorker?.postMessage({
                                    type: WorkerMessageType.KEEPALIVE_RESULT,
                                    statusCode: res.statusCode,
                                });
                            }
                            catch (err) {
                                const error = {
                                    headers: {
                                        trackingid: err.headers?.trackingid,
                                        'retry-after': err.headers['retry-after'],
                                    },
                                    statusCode: err.statusCode,
                                    statusText: err.statusText,
                                    type: err.type,
                                };
                                this.webWorker?.postMessage({
                                    type: WorkerMessageType.KEEPALIVE_RESULT,
                                    err: error,
                                });
                            }
                        }
                        if (event.data.type === WorkerMessageType.KEEPALIVE_SUCCESS) {
                            log.info(`Sent Keepalive, status: ${event.data.statusCode}`, logContext);
                            this.lineEmitter(LINE_EVENTS.RECONNECTED);
                        }
                        if (event.data.type === WorkerMessageType.KEEPALIVE_FAILURE) {
                            const error = event.data.err;
                            log.warn(`Keep-alive missed ${event.data.keepAliveRetryCount} times. Status -> ${error.statusCode} `, logContext);
                            const { finalError: abort } = await handleRegistrationErrors(error, (clientError, finalError) => {
                                if (finalError) {
                                    this.lineEmitter(LINE_EVENTS.ERROR, undefined, clientError);
                                }
                                this.metricManager.submitRegistrationMetric(METRIC_EVENT.KEEPALIVE_ERROR, REG_ACTION.KEEPALIVE_FAILURE, METRIC_TYPE.BEHAVIORAL, KEEPALIVE_UTIL, serverType, error.headers?.trackingid ?? '', event.data.keepAliveRetryCount, clientError);
                            }, { method: KEEPALIVE_UTIL, file: REGISTRATION_FILE }, (retryAfter, retryCaller) => this.handle429Retry(retryAfter, retryCaller));
                            if (abort || event.data.keepAliveRetryCount >= RETRY_COUNT_THRESHOLD) {
                                this.failoverImmediately = this.isCCFlow;
                                this.setStatus(RegistrationStatus.INACTIVE);
                                this.clearKeepaliveTimer();
                                this.clearFailbackTimer();
                                this.lineEmitter(LINE_EVENTS.UNREGISTERED);
                                await uploadLogs();
                                if (!abort) {
                                    await this.reconnectOnFailure(RECONNECT_ON_FAILURE_UTIL);
                                }
                                else if (error.statusCode === 404) {
                                    this.handle404KeepaliveFailure(KEEPALIVE_UTIL);
                                }
                            }
                            else {
                                this.lineEmitter(LINE_EVENTS.RECONNECTING);
                            }
                        }
                    };
                }
            }
        });
    }
    clearKeepaliveTimer() {
        if (this.webWorker) {
            this.webWorker.postMessage({ type: WorkerMessageType.CLEAR_KEEPALIVE });
            this.webWorker.terminate();
            this.webWorker = undefined;
        }
    }
    isReconnectPending() {
        return this.reconnectPending;
    }
    async deregister(closeMobiusWss = false) {
        const loggerContext = {
            file: REGISTRATION_FILE,
            method: METHODS.DEREGISTER,
        };
        log.info(METHOD_START_MESSAGE, loggerContext);
        try {
            await this.deleteRegistration(this.activeMobiusUrl, this.deviceInfo.device?.deviceId, this.deviceInfo.device?.clientDeviceUri);
            log.log('Registration successfully deregistered', loggerContext);
        }
        catch (err) {
            log.warn(`Delete failed with Mobius: ${JSON.stringify(err)}`, loggerContext);
        }
        if (closeMobiusWss) {
            await this.apiRequest.disconnectFromMobiusSocket({
                code: 3050,
                reason: 'done (permanent)',
            });
            log.log('Mobius socket disconnect complete after deregistration', loggerContext);
        }
        this.clearKeepaliveTimer();
        this.setStatus(RegistrationStatus.INACTIVE);
        this.clearFailoverState();
    }
    isRegRetry() {
        return this.registerRetry;
    }
    setRegRetry(value) {
        this.registerRetry = value;
    }
    getExistingDevice(restoreData) {
        if (restoreData.devices && restoreData.devices.length > 0) {
            this.deviceInfo = {
                userId: restoreData.userId,
                device: restoreData.devices[0],
                keepaliveInterval: DEFAULT_KEEPALIVE_INTERVAL,
                rehomingIntervalMax: DEFAULT_REHOMING_INTERVAL_MAX,
                rehomingIntervalMin: DEFAULT_REHOMING_INTERVAL_MIN,
            };
            const stringToReplace = `${DEVICES_ENDPOINT_RESOURCE}/${restoreData.devices[0].deviceId}`;
            let uri = restoreData.devices[0].uri.replace(stringToReplace, '');
            if (this.apiRequest.isSocketEnabled()) {
                uri = uri.replace('https://', 'wss://');
                uri = !uri.endsWith('/') ? `${uri}/` : uri;
            }
            this.setActiveMobiusUrl(uri);
            this.registrationStatus = RegistrationStatus.ACTIVE;
            return true;
        }
        return false;
    }
    async reconnectOnFailure(caller) {
        log.info(METHOD_START_MESSAGE, { method: METHODS.RECONNECT_ON_FAILURE, file: REGISTRATION_FILE });
        this.reconnectPending = false;
        if (!this.isDeviceRegistered()) {
            if (Object.keys(this.callManager.getActiveCalls()).length === 0) {
                const abort = await this.restorePreviousRegistration(caller);
                if (!abort && !this.isDeviceRegistered()) {
                    await this.restartRegistration(caller);
                }
            }
            else {
                this.reconnectPending = true;
                log.info('Active call(s) present, deferred reconnect till call cleanup.', {
                    file: REGISTRATION_FILE,
                    method: METHODS.RECONNECT_ON_FAILURE,
                });
            }
        }
    }
    async handleRegistrationDownEvent(event) {
        const loggerContext = {
            file: REGISTRATION_FILE,
            method: METHODS.HANDLE_REGISTRATION_DOWN_EVENT,
        };
        log.info(`Registration down received - trackingId: ${event?.trackingId ?? 'unknown'}, eventId: ${event?.eventId ?? 'unknown'}`, loggerContext);
        const [activeCall] = Object.values(this.callManager.getActiveCalls());
        activeCall?.end();
        await this.performRegistrationDownCleanup(METHODS.HANDLE_REGISTRATION_DOWN_EVENT);
    }
    async performRegistrationDownCleanup(caller) {
        const loggerContext = {
            file: REGISTRATION_FILE,
            method: METHODS.HANDLE_REGISTRATION_DOWN_EVENT,
        };
        log.info(`[${caller}] : Running registration-down cleanup`, loggerContext);
        await this.mutex.runExclusive(async () => {
            this.clearFailbackTimer();
            this.clearKeepaliveTimer();
            this.reconnectPending = false;
            this.scheduled429Retry = false;
            this.failoverImmediately = false;
            this.retryAfter = undefined;
            this.registerRetry = false;
            this.clearFailoverState();
            this.setStatus(RegistrationStatus.INACTIVE);
            if (this.apiRequest.isSocketEnabled()) {
                try {
                    await this.apiRequest.disconnectFromMobiusSocket({
                        code: 3050,
                        reason: 'done (permanent)',
                    });
                    log.log('Mobius socket disconnect complete after registration-down', loggerContext);
                }
                catch (err) {
                    log.warn(`Mobius socket disconnect failed after registration-down: ${String(err)}`, loggerContext);
                }
            }
            this.lineEmitter(LINE_EVENTS.UNREGISTERED);
        });
    }
}
export const createRegistration = (webex, serviceData, mutex, lineEmitter, logLevel, jwe) => new Registration(webex, serviceData, mutex, lineEmitter, logLevel, jwe);
