text: /* global Olm */
  import { getLogger } from '@jitsi/logger';
  import base64js from 'base64-js';
  import isEqual from 'lodash.isequal';
  import { v4 as uuidv4 } from 'uuid';
  import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
  import Deferred from '../util/Deferred';
  import Listenable from '../util/Listenable';
  import { FEATURE_E2EE, JITSI_MEET_MUC_TYPE } from '../xmpp/xmpp';
  const logger = getLogger(__filename);
  const REQ_TIMEOUT = 5 * 1000;
  const OLM_MESSAGE_TYPE = 'olm';
  const OLM_MESSAGE_TYPES = {
    ERROR: 'error',
    KEY_INFO: 'key-info',
    KEY_INFO_ACK: 'key-info-ack',
    SESSION_ACK: 'session-ack',
    SESSION_INIT: 'session-init'
  };
  const kOlmData = Symbol('OlmData');
  const OlmAdapterEvents = {
    OLM_ID_KEY_READY: 'olm.id_key_ready',
    PARTICIPANT_E2EE_CHANNEL_READY: 'olm.participant_e2ee_channel_ready',
    PARTICIPANT_KEY_UPDATED: 'olm.partitipant_key_updated'
  };
  /**
   * This class implements an End-to-End Encrypted communication channel between every two peers
   * in the conference. This channel uses libolm to achieve E2EE.
   *
   * The created channel is then used to exchange the secret key that each participant will use
   * to encrypt the actual media (see {@link E2EEContext}).
   *
   * A simple JSON message based protocol is implemented, which follows a request - response model:
   * - session-init: Initiates an olm session establishment procedure. This message will be sent
   *                 by the participant who just joined, to everyone else.
   * - session-ack: Completes the olm session etablishment. This messsage may contain ancilliary
   *                encrypted data, more specifically the sender's current key.
   * - key-info: Includes the sender's most up to date key information.
   * - key-info-ack: Acknowledges the reception of a key-info request. In addition, it may contain
   *                 the sender's key information, if available.
   * - error: Indicates a request processing error has occurred.
   *
   * These requessts and responses are transport independent. Currently they are sent using XMPP
   * MUC private messages.
   */
  
  export class OlmAdapter extends Listenable {
    /**
     * Creates an adapter instance for the given conference.
     */
    constructor(conference) {
      super();
      this._conf = conference;
      this._init = new Deferred();
      this._key = undefined;
      this._keyIndex = -1;
      this._reqs = new Map();
      this._sessionInitialization = undefined;
  
      if (OlmAdapter.isSupported()) {
        this._bootstrapOlm();
  
        this._conf.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, this._onEndpointMessageReceived.bind(this));
  
        this._conf.on(JitsiConferenceEvents.CONFERENCE_LEFT, this._onConferenceLeft.bind(this));
  
        this._conf.on(JitsiConferenceEvents.USER_LEFT, this._onParticipantLeft.bind(this));
  
        this._conf.on(JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED, this._onParticipantPropertyChanged.bind(this));
      } else {
        this._init.reject(new Error('Olm not supported'));
      }
    }
    /**
     * Starts new olm sessions with every other participant that has the participantId "smaller" the localParticipantId.
     */
  
  
    async initSessions() {
      if (this._sessionInitialization) {
        throw new Error('OlmAdapter initSessions called multiple times');
      } else {
        this._sessionInitialization = new Deferred();
        await this._init;
        const promises = [];
  
        const localParticipantId = this._conf.myUserId();
  
        for (const participant of this._conf.getParticipants()) {
          if (participant.hasFeature(FEATURE_E2EE) && localParticipantId < participant.getId()) {
            promises.push(this._sendSessionInit(participant));
          }
        }
  
        await Promise.allSettled(promises); // TODO: retry failed ones.
  
        this._sessionInitialization.resolve();
  
        this._sessionInitialization = undefined;
      }
    }
    /**
     * Indicates if olm is supported on the current platform.
     *
     * @returns {boolean}
     */
  
  
    static isSupported() {
      return typeof window.Olm !== 'undefined';
    }
    /**
     * Updates the current participant key and distributes it to all participants in the conference
     * by sending a key-info message.
     *
     * @param {Uint8Array|boolean} key - The new key.
     * @retrns {Promise<Number>}
     */
  
  
    async updateKey(key) {
      // Store it locally for new sessions.
      this._key = key;
      this._keyIndex++; // Broadcast it.
  
      const promises = [];
  
      for (const participant of this._conf.getParticipants()) {
        const pId = participant.getId();
  
        const olmData = this._getParticipantOlmData(participant); // TODO: skip those who don't support E2EE.
  
  
        if (!olmData.session) {
          logger.warn(`Tried to send key to participant ${pId} but we have no session`); // eslint-disable-next-line no-continue
  
          continue;
        }
  
        const uuid = uuidv4();
        const data = {
          [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
          olm: {
            type: OLM_MESSAGE_TYPES.KEY_INFO,
            data: {
              ciphertext: this._encryptKeyInfo(olmData.session),
              uuid
            }
          }
        };
        const d = new Deferred();
        d.setRejectTimeout(REQ_TIMEOUT);
        d.catch(() => {
          this._reqs.delete(uuid);
        });
  
        this._reqs.set(uuid, d);
  
        promises.push(d);
  
        this._sendMessage(data, pId);
      }
  
      await Promise.allSettled(promises); // TODO: retry failed ones?
  
      return this._keyIndex;
    }
    /**
     * Updates the current participant key.
     * @param {Uint8Array|boolean} key - The new key.
     * @returns {number}
    */
  
  
    updateCurrentKey(key) {
      this._key = key;
      return this._keyIndex;
    }
    /**
     * Frees the olmData session for the given participant.
     *
     */
  
  
    clearParticipantSession(participant) {
      const olmData = this._getParticipantOlmData(participant);
  
      if (olmData.session) {
        olmData.session.free();
        olmData.session = undefined;
      }
    }
    /**
     * Frees the olmData sessions for all participants.
     *
     */
  
  
    clearAllParticipantsSessions() {
      for (const participant of this._conf.getParticipants()) {
        this.clearParticipantSession(participant);
      }
    }
    /**
     * Internal helper to bootstrap the olm library.
     *
     * @returns {Promise<void>}
     * @private
     */
  
  
    async _bootstrapOlm() {
      logger.debug('Initializing Olm...');
  
      try {
        await Olm.init();
        this._olmAccount = new Olm.Account();
  
        this._olmAccount.create();
  
        const idKeys = JSON.parse(this._olmAccount.identity_keys());
        this._idKey = idKeys.curve25519;
        logger.debug(`Olm ${Olm.get_library_version().join('.')} initialized`);
  
        this._init.resolve();
  
        this._onIdKeyReady(this._idKey);
      } catch (e) {
        logger.error('Failed to initialize Olm', e);
  
        this._init.reject(e);
      }
    }
    /**
     * Publishes our own Olmn id key in presence.
     * @private
     */
  
  
    _onIdKeyReady(idKey) {
      logger.debug(`Olm id key ready: ${idKey}`); // Publish it in presence.
  
      this._conf.setLocalParticipantProperty('e2ee.idKey', idKey);
    }
    /**
     * Event posted when the E2EE signalling channel has been established with the given participant.
     * @private
     */
  
  
    _onParticipantE2EEChannelReady(id) {
      logger.debug(`E2EE channel with participant ${id} is ready`);
    }
    /**
     * Internal helper for encrypting the current key information for a given participant.
     *
     * @param {Olm.Session} session - Participant's session.
     * @returns {string} - The encrypted text with the key information.
     * @private
     */
  
  
    _encryptKeyInfo(session) {
      const keyInfo = {};
  
      if (this._key !== undefined) {
        keyInfo.key = this._key ? base64js.fromByteArray(this._key) : false;
        keyInfo.keyIndex = this._keyIndex;
      }
  
      return session.encrypt(JSON.stringify(keyInfo));
    }
    /**
     * Internal helper for getting the olm related data associated with a participant.
     *
     * @param {JitsiParticipant} participant - Participant whose data wants to be extracted.
     * @returns {Object}
     * @private
     */
  
  
    _getParticipantOlmData(participant) {
      participant[kOlmData] = participant[kOlmData] || {};
      return participant[kOlmData];
    }
    /**
     * Handles leaving the conference, cleaning up olm sessions.
     *
     * @private
     */
  
  
    async _onConferenceLeft() {
      logger.debug('Conference left');
      await this._init;
  
      for (const participant of this._conf.getParticipants()) {
        this._onParticipantLeft(participant.getId(), participant);
      }
  
      if (this._olmAccount) {
        this._olmAccount.free();
  
        this._olmAccount = undefined;
      }
    }
    /**
     * Main message handler. Handles 1-to-1 messages received from other participants
     * and send the appropriate replies.
     *
     * @private
     */
  
  
    async _onEndpointMessageReceived(participant, payload) {
      if (payload[JITSI_MEET_MUC_TYPE] !== OLM_MESSAGE_TYPE) {
        return;
      }
  
      if (!payload.olm) {
        logger.warn('Incorrectly formatted message');
        return;
      }
  
      await this._init;
      const msg = payload.olm;
      const pId = participant.getId();
  
      const olmData = this._getParticipantOlmData(participant);
  
      switch (msg.type) {
        case OLM_MESSAGE_TYPES.SESSION_INIT:
          {
            if (olmData.session) {
              logger.warn(`Participant ${pId} already has a session`);
  
              this._sendError(participant, 'Session already established');
            } else {
              // Create a session for communicating with this participant.
              const session = new Olm.Session();
              session.create_outbound(this._olmAccount, msg.data.idKey, msg.data.otKey);
              olmData.session = session; // Send ACK
  
              const ack = {
                [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
                olm: {
                  type: OLM_MESSAGE_TYPES.SESSION_ACK,
                  data: {
                    ciphertext: this._encryptKeyInfo(session),
                    uuid: msg.data.uuid
                  }
                }
              };
  
              this._sendMessage(ack, pId);
  
              this._onParticipantE2EEChannelReady(pId);
            }
  
            break;
          }
  
        case OLM_MESSAGE_TYPES.SESSION_ACK:
          {
            if (olmData.session) {
              logger.warn(`Participant ${pId} already has a session`);
  
              this._sendError(participant, 'No session found');
            } else if (msg.data.uuid === olmData.pendingSessionUuid) {
              const {
                ciphertext
              } = msg.data;
  
              const d = this._reqs.get(msg.data.uuid);
  
              const session = new Olm.Session();
              session.create_inbound(this._olmAccount, ciphertext.body); // Remove OT keys that have been used to setup this session.
  
              this._olmAccount.remove_one_time_keys(session); // Decrypt first message.
  
  
              const data = session.decrypt(ciphertext.type, ciphertext.body);
              olmData.session = session;
              olmData.pendingSessionUuid = undefined;
  
              this._onParticipantE2EEChannelReady(pId);
  
              this._reqs.delete(msg.data.uuid);
  
              d.resolve();
              const json = safeJsonParse(data);
  
              if (json.key) {
                const key = base64js.toByteArray(json.key);
                const keyIndex = json.keyIndex;
                olmData.lastKey = key;
                this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_KEY_UPDATED, pId, key, keyIndex);
              }
            } else {
              logger.warn('Received ACK with the wrong UUID');
  
              this._sendError(participant, 'Invalid UUID');
            }
  
            break;
          }
  
        case OLM_MESSAGE_TYPES.ERROR:
          {
            logger.error(msg.data.error);
            break;
          }
  
        case OLM_MESSAGE_TYPES.KEY_INFO:
          {
            if (olmData.session) {
              const {
                ciphertext
              } = msg.data;
              const data = olmData.session.decrypt(ciphertext.type, ciphertext.body);
              const json = safeJsonParse(data);
  
              if (json.key !== undefined && json.keyIndex !== undefined) {
                const key = json.key ? base64js.toByteArray(json.key) : false;
                const keyIndex = json.keyIndex;
  
                if (!isEqual(olmData.lastKey, key)) {
                  olmData.lastKey = key;
                  this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_KEY_UPDATED, pId, key, keyIndex);
                } // Send ACK.
  
  
                const ack = {
                  [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
                  olm: {
                    type: OLM_MESSAGE_TYPES.KEY_INFO_ACK,
                    data: {
                      ciphertext: this._encryptKeyInfo(olmData.session),
                      uuid: msg.data.uuid
                    }
                  }
                };
  
                this._sendMessage(ack, pId);
              }
            } else {
              logger.debug(`Received key info message from ${pId} but we have no session for them!`);
  
              this._sendError(participant, 'No session found while processing key-info');
            }
  
            break;
          }
  
        case OLM_MESSAGE_TYPES.KEY_INFO_ACK:
          {
            if (olmData.session) {
              const {
                ciphertext
              } = msg.data;
              const data = olmData.session.decrypt(ciphertext.type, ciphertext.body);
              const json = safeJsonParse(data);
  
              if (json.key !== undefined && json.keyIndex !== undefined) {
                const key = json.key ? base64js.toByteArray(json.key) : false;
                const keyIndex = json.keyIndex;
  
                if (!isEqual(olmData.lastKey, key)) {
                  olmData.lastKey = key;
                  this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_KEY_UPDATED, pId, key, keyIndex);
                }
              }
  
              const d = this._reqs.get(msg.data.uuid);
  
              this._reqs.delete(msg.data.uuid);
  
              d.resolve();
            } else {
              logger.debug(`Received key info ack message from ${pId} but we have no session for them!`);
  
              this._sendError(participant, 'No session found while processing key-info-ack');
            }
  
            break;
          }
      }
    }
    /**
     * Handles a participant leaving. When a participant leaves their olm session is destroyed.
     *
     * @private
     */
  
  
    _onParticipantLeft(id, participant) {
      logger.debug(`Participant ${id} left`);
      this.clearParticipantSession(participant);
    }
    /**
    * Handles an update in a participant's presence property.
    *
    * @param {JitsiParticipant} participant - The participant.
    * @param {string} name - The name of the property that changed.
    * @param {*} oldValue - The property's previous value.
    * @param {*} newValue - The property's new value.
    * @private
    */
  
  
    async _onParticipantPropertyChanged(participant, name, oldValue, newValue) {
      switch (name) {
        case 'e2ee.enabled':
          if (newValue && this._conf.isE2EEEnabled()) {
            const localParticipantId = this._conf.myUserId();
  
            const participantId = participant.getId();
            const participantFeatures = await participant.getFeatures();
  
            if (participantFeatures.has(FEATURE_E2EE) && localParticipantId < participantId) {
              if (this._sessionInitialization) {
                await this._sessionInitialization;
              }
  
              await this._sendSessionInit(participant);
  
              const olmData = this._getParticipantOlmData(participant);
  
              const uuid = uuidv4();
              const data = {
                [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
                olm: {
                  type: OLM_MESSAGE_TYPES.KEY_INFO,
                  data: {
                    ciphertext: this._encryptKeyInfo(olmData.session),
                    uuid
                  }
                }
              };
  
              this._sendMessage(data, participantId);
            }
          }
  
          break;
      }
    }
    /**
     * Builds and sends an error message to the target participant.
     *
     * @param {JitsiParticipant} participant - The target participant.
     * @param {string} error - The error message.
     * @returns {void}
     */
  
  
    _sendError(participant, error) {
      const pId = participant.getId();
      const err = {
        [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
        olm: {
          type: OLM_MESSAGE_TYPES.ERROR,
          data: {
            error
          }
        }
      };
  
      this._sendMessage(err, pId);
    }
    /**
     * Internal helper to send the given object to the given participant ID.
     * This function merely exists so the transport can be easily swapped.
     * Currently messages are transmitted via XMPP MUC private messages.
     *
     * @param {object} data - The data that will be sent to the target participant.
     * @param {string} participantId - ID of the target participant.
     */
  
  
    _sendMessage(data, participantId) {
      this._conf.sendMessage(data, participantId);
    }
    /**
     * Builds and sends the session-init request to the target participant.
     *
     * @param {JitsiParticipant} participant - Participant to whom we'll send the request.
     * @returns {Promise} - The promise will be resolved when the session-ack is received.
     * @private
     */
  
  
    _sendSessionInit(participant) {
      const pId = participant.getId();
  
      const olmData = this._getParticipantOlmData(participant);
  
      if (olmData.session) {
        logger.warn(`Tried to send session-init to ${pId} but we already have a session`);
        return Promise.reject();
      }
  
      if (olmData.pendingSessionUuid !== undefined) {
        logger.warn(`Tried to send session-init to ${pId} but we already have a pending session`);
        return Promise.reject();
      } // Generate a One Time Key.
  
  
      this._olmAccount.generate_one_time_keys(1);
  
      const otKeys = JSON.parse(this._olmAccount.one_time_keys());
      const otKey = Object.values(otKeys.curve25519)[0];
  
      if (!otKey) {
        return Promise.reject(new Error('No one-time-keys generated'));
      } // Mark the OT keys (one really) as published so they are not reused.
  
  
      this._olmAccount.mark_keys_as_published();
  
      const uuid = uuidv4();
      const init = {
        [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
        olm: {
          type: OLM_MESSAGE_TYPES.SESSION_INIT,
          data: {
            idKey: this._idKey,
            otKey,
            uuid
          }
        }
      };
      const d = new Deferred();
      d.setRejectTimeout(REQ_TIMEOUT);
      d.catch(() => {
        this._reqs.delete(uuid);
  
        olmData.pendingSessionUuid = undefined;
      });
  
      this._reqs.set(uuid, d);
  
      this._sendMessage(init, pId); // Store the UUID for matching with the ACK.
  
  
      olmData.pendingSessionUuid = uuid;
      return d;
    }
  
  }
  /**
   * Helper to ensure JSON parsing always returns an object.
   *
   * @param {string} data - The data that needs to be parsed.
   * @returns {object} - Parsed data or empty object in case of failure.
   */
  
  function safeJsonParse(data) {
    try {
      return JSON.parse(data);
    } catch (e) {
      return {};
    }
  }
  
  OlmAdapter.events = OlmAdapterEvents;