import { cloneDeep } from 'lodash-es';

import { IReduxState, IStore } from '../../app/types';
import { conferenceLeft, conferenceWillLeave, redirect } from '../conference/actions';
import { getCurrentConference } from '../conference/functions';
import { IConfigState } from '../config/reducer';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import { inIframe } from '../util/iframeUtils';
import { parseURLParams } from '../util/parseURLParams';
import {
    appendURLParam,
    getBackendSafeRoomName
} from '../util/uri';

import {
    CONNECTION_DISCONNECTED,
    CONNECTION_ESTABLISHED,
    CONNECTION_FAILED,
    CONNECTION_PROPERTIES_UPDATED,
    CONNECTION_WILL_CONNECT,
    SET_LOCATION_URL,
    SET_PREFER_VISITOR
} from './actionTypes';
import { JITSI_CONNECTION_URL_KEY } from './constants';
import logger from './logger';
import { ConnectionFailedError, IIceServers } from './types';

/**
 * The options that will be passed to the JitsiConnection instance.
 */
interface IOptions extends IConfigState {
    iceServersOverride?: IIceServers;
    preferVisitor?: boolean;
}

/**
 * Create an action for when the signaling connection has been lost.
 *
 * @param {JitsiConnection} connection - The {@code JitsiConnection} which
 * disconnected.
 * @private
 * @returns {{
 *     type: CONNECTION_DISCONNECTED,
 *     connection: JitsiConnection
 * }}
 */
export function connectionDisconnected(connection?: Object) {
    return {
        type: CONNECTION_DISCONNECTED,
        connection
    };
}

/**
 * Create an action for when the signaling connection has been established.
 *
 * @param {JitsiConnection} connection - The {@code JitsiConnection} which was
 * established.
 * @param {number} timeEstablished - The time at which the
 * {@code JitsiConnection} which was established.
 * @public
 * @returns {{
 *     type: CONNECTION_ESTABLISHED,
 *     connection: JitsiConnection,
 *     timeEstablished: number
 * }}
 */
export function connectionEstablished(
        connection: Object, timeEstablished: number) {
    return {
        type: CONNECTION_ESTABLISHED,
        connection,
        timeEstablished
    };
}

/**
 * Create an action for when the signaling connection could not be created.
 *
 * @param {JitsiConnection} connection - The {@code JitsiConnection} which
 * failed.
 * @param {ConnectionFailedError} error - Error.
 * @public
 * @returns {{
 *     type: CONNECTION_FAILED,
 *     connection: JitsiConnection,
 *     error: ConnectionFailedError
 * }}
 */
export function connectionFailed(
        connection: Object,
        error: ConnectionFailedError) {
    const { credentials } = error;

    if (credentials && !Object.keys(credentials).length) {
        error.credentials = undefined;
    }

    return {
        type: CONNECTION_FAILED,
        connection,
        error
    };
}

/**
 * Constructs options to be passed to the constructor of {@code JitsiConnection}
 * based on the redux state.
 *
 * @param {Object} state - The redux state.
 * @returns {Object} The options to be passed to the constructor of
 * {@code JitsiConnection}.
 */
export function constructOptions(state: IReduxState) {
    // Deep clone the options to make sure we don't modify the object in the
    // redux store.
    const options: IOptions = cloneDeep(state['features/base/config']);

    const { locationURL, preferVisitor } = state['features/base/connection'];
    const params = parseURLParams(locationURL || '');
    const iceServersOverride = params['iceServers.replace'];

    // Allow iceServersOverride only when jitsi-meet is in an iframe.
    if (inIframe() && iceServersOverride) {
        options.iceServersOverride = iceServersOverride;
    }

    const { bosh, preferBosh, flags } = options;
    let { websocket } = options;

    if (preferBosh) {
        websocket = undefined;
    }

    // WebSocket is preferred over BOSH.
    const serviceUrl = websocket || bosh;

    logger.log(`Using service URL ${serviceUrl}`);

    // Append room to the URL's search.
    const { room } = state['features/base/conference'];

    if (serviceUrl && room) {
        const roomName = getBackendSafeRoomName(room);

        options.serviceUrl = appendURLParam(serviceUrl, 'room', roomName ?? '');

        if (options.websocketKeepAliveUrl) {
            options.websocketKeepAliveUrl = appendURLParam(options.websocketKeepAliveUrl, 'room', roomName ?? '');
        }
        if (options.conferenceRequestUrl) {
            options.conferenceRequestUrl = appendURLParam(options.conferenceRequestUrl, 'room', roomName ?? '');
        }
    }

    if (preferVisitor) {
        options.preferVisitor = true;
    }

    // Enable ssrc-rewriting by default.
    if (typeof flags?.ssrcRewritingEnabled === 'undefined') {
        const { ...otherFlags } = flags ?? {};

        options.flags = {
            ...otherFlags,
            ssrcRewritingEnabled: true
        };
    }

    return options;
}

/**
 * Sets the location URL of the application, connection, conference, etc.
 *
 * @param {URL} [locationURL] - The location URL of the application,
 * connection, conference, etc.
 * @returns {{
 *     type: SET_LOCATION_URL,
 *     locationURL: URL
 * }}
 */
export function setLocationURL(locationURL?: URL) {
    return {
        type: SET_LOCATION_URL,
        locationURL
    };
}

/**
 * To change prefer visitor in the store. Used later to decide what to request from jicofo on connection.
 *
 * @param {boolean} preferVisitor - The value to set.
 * @returns {{
 *     type: SET_PREFER_VISITOR,
 *     preferVisitor: boolean
 * }}
 */
export function setPreferVisitor(preferVisitor: boolean) {
    return {
        type: SET_PREFER_VISITOR,
        preferVisitor
    };
}

/**
 * Opens new connection.
 *
 * @param {string} [id] - The XMPP user's ID (e.g. {@code user@server.com}).
 * @param {string} [password] - The XMPP user's password.
 * @returns {Function}
 */
export function _connectInternal(id?: string, password?: string) {
    return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
        const state = getState();
        const options = constructOptions(state);
        const { locationURL } = state['features/base/connection'];
        const { jwt } = state['features/base/jwt'];

        const connection = new JitsiMeetJS.JitsiConnection(options.appId, jwt, options);

        connection[JITSI_CONNECTION_URL_KEY] = locationURL;

        dispatch(_connectionWillConnect(connection));

        return new Promise((resolve, reject) => {
            connection.addEventListener(
                JitsiConnectionEvents.CONNECTION_DISCONNECTED,
                _onConnectionDisconnected);
            connection.addEventListener(
                JitsiConnectionEvents.CONNECTION_ESTABLISHED,
                _onConnectionEstablished);
            connection.addEventListener(
                JitsiConnectionEvents.CONNECTION_FAILED,
                _onConnectionFailed);
            connection.addEventListener(
                JitsiConnectionEvents.CONNECTION_REDIRECTED,
                _onConnectionRedirected);
            connection.addEventListener(
                JitsiConnectionEvents.PROPERTIES_UPDATED,
                _onPropertiesUpdate);

            /**
             * Unsubscribe the connection instance from
             * {@code CONNECTION_DISCONNECTED} and {@code CONNECTION_FAILED} events.
             *
             * @returns {void}
             */
            function unsubscribe() {
                connection.removeEventListener(
                    JitsiConnectionEvents.CONNECTION_DISCONNECTED, _onConnectionDisconnected);
                connection.removeEventListener(JitsiConnectionEvents.CONNECTION_FAILED, _onConnectionFailed);
                connection.removeEventListener(JitsiConnectionEvents.PROPERTIES_UPDATED, _onPropertiesUpdate);
            }

            /**
             * Dispatches {@code CONNECTION_DISCONNECTED} action when connection is
             * disconnected.
             *
             * @private
             * @returns {void}
             */
            function _onConnectionDisconnected() {
                unsubscribe();
                dispatch(connectionDisconnected(connection));
                resolve(connection);
            }

            /**
             * Rejects external promise when connection fails.
             *
             * @param {JitsiConnectionErrors} err - Connection error.
             * @param {string} [message] - Error message supplied by lib-jitsi-meet.
             * @param {Object} [credentials] - The invalid credentials that were
             * used to authenticate and the authentication failed.
             * @param {string} [credentials.jid] - The XMPP user's ID.
             * @param {string} [credentials.password] - The XMPP user's password.
             * @param {Object} details - Additional information about the error.
             * @private
             * @returns {void}
             */
            function _onConnectionFailed( // eslint-disable-line max-params
                    err: string,
                    message: string,
                    credentials: any,
                    details: Object) {
                unsubscribe();

                dispatch(connectionFailed(connection, {
                    credentials,
                    details,
                    name: err,
                    message
                }));

                reject(err);
            }

            /**
             * Resolves external promise when connection is established.
             *
             * @private
             * @returns {void}
             */
            function _onConnectionEstablished() {
                connection.removeEventListener(JitsiConnectionEvents.CONNECTION_ESTABLISHED, _onConnectionEstablished);
                dispatch(connectionEstablished(connection, Date.now()));
                resolve(connection);
            }

            /**
             * Connection was redirected.
             *
             * @param {string|undefined} vnode - The vnode to connect to.
             * @param {string} focusJid - The focus jid to use.
             * @param {string|undefined} username - The username to use when joining. This is after promotion from
             * visitor to main participant.
             * @private
             * @returns {void}
             */
            function _onConnectionRedirected(vnode: string, focusJid: string, username: string) {
                connection.removeEventListener(JitsiConnectionEvents.CONNECTION_REDIRECTED, _onConnectionRedirected);
                dispatch(redirect(vnode, focusJid, username));
            }

            /**
             * Connection properties were updated.
             *
             * @param {Object} properties - The properties which were updated.
             * @private
             * @returns {void}
             */
            function _onPropertiesUpdate(properties: object) {
                dispatch(_propertiesUpdate(properties));
            }

            // in case of configured http url for conference request we need the room name
            const name = getBackendSafeRoomName(state['features/base/conference'].room);

            connection.connect({
                id,
                password,
                name
            });
        });
    };
}

/**
 * Create an action for when a connection will connect.
 *
 * @param {JitsiConnection} connection - The {@code JitsiConnection} which will
 * connect.
 * @private
 * @returns {{
 *     type: CONNECTION_WILL_CONNECT,
 *     connection: JitsiConnection
 * }}
 */
function _connectionWillConnect(connection: Object) {
    return {
        type: CONNECTION_WILL_CONNECT,
        connection
    };
}

/**
 * Create an action for when connection properties are updated.
 *
 * @param {Object} properties - The properties which were updated.
 * @private
 * @returns {{
 *     type: CONNECTION_PROPERTIES_UPDATED,
 *     properties: Object
 * }}
 */
function _propertiesUpdate(properties: object) {
    return {
        type: CONNECTION_PROPERTIES_UPDATED,
        properties
    };
}

/**
 * Closes connection.
 *
 * @param {boolean} isRedirect - Indicates if the action has been dispatched as part of visitor promotion.
 *
 * @returns {Function}
 */
export function disconnect(isRedirect?: boolean) {
    return (dispatch: IStore['dispatch'], getState: IStore['getState']): Promise<void> => {
        const state = getState();

        // The conference we have already joined or are joining.
        const conference_ = getCurrentConference(state);

        // Promise which completes when the conference has been left and the
        // connection has been disconnected.
        let promise;

        // Leave the conference.
        if (conference_) {
            // In a fashion similar to JitsiConference's CONFERENCE_LEFT event
            // (and the respective Redux action) which is fired after the
            // conference has been left, notify the application about the
            // intention to leave the conference.
            dispatch(conferenceWillLeave(conference_, isRedirect));

            promise
                = conference_.leave()
                .catch((error: Error) => {
                    logger.warn(
                        'JitsiConference.leave() rejected with:',
                        error);

                    // The library lib-jitsi-meet failed to make the
                    // JitsiConference leave. Which may be because
                    // JitsiConference thinks it has already left.
                    // Regardless of the failure reason, continue in
                    // jitsi-meet as if the leave has succeeded.
                    dispatch(conferenceLeft(conference_));
                });
        } else {
            promise = Promise.resolve();
        }

        // Disconnect the connection.
        const { connecting, connection } = state['features/base/connection'];

        // The connection we have already connected or are connecting.
        const connection_ = connection || connecting;

        if (connection_) {
            promise = promise.then(() => connection_.disconnect());
        } else {
            logger.info('No connection found while disconnecting.');
        }

        return promise;
    };
}
