/* global API, asyncStorage, ErrorManager, debug */
/* jshint maxstatements:50 */
'use strict';

function Proxy() {
    var KEY_AUTH_COOKIE = 'authCookie';
    var KEY_USER_EMAIL = 'userEmail';
    var KEY_APPS_LIST = 'applicationsList';
    var KEY_PREFERENCES_LIST = 'preferencesList';
    var KEY_LAST_LOGIN = 'lastLogin';
    var KEY_LOG_FILTER_TYPES = 'logFilter';
    var KEY_ORANGE_RIBBON = 'orangeRibbon';

    var operationsMap = {};
    var operationsRawData = null;
    var preferencesMap = {};

    function initialize(callback) {
        // we check if the auth cookie is stored, and set it for future API calls
        _getAuthCookie(function retrieved(cookie) {
            API.setAuthCookie(cookie);
            if (callback) {
                callback(cookie);
            }
        });
    }

    /**
     *  Check if the application is connected to internet
     */
    function _isOnline() {
        return navigator.onLine;
    }

    /**
     *  Retrieves the authentication cookie from storage, if any.
     *  @parameter callback To be called when action is done with response/error
     */
    function _getAuthCookie(callback) {
        asyncStorage.getItem(KEY_AUTH_COOKIE, function retrieved(item) {
            callback(item);
        });
    }

    function _isEqual(newObject, oldObject) {

        for (var key in newObject) {
            if (typeof newObject[key] === typeof oldObject[key]) {
                if (typeof newObject[key] !== 'object' &&
                        newObject[key] !== oldObject[key]) {
                    return false;
                }
            } else {
                return false;
            }
        }
        return true;
    }

    /**
     * Format the server response for list of operations into
     * a plain structure for direct acces and compares it with the
     * previously stored list.
     * Each element of this structure will have the following:
     * 'OPx' : {
     *    parent: 'OPw',
     *    ... (op details, without children ops)
     *    operations: [OPy(obj), Opz(obj), Opy(obj)],
     *    globalApplication: { // Top operation
     *      name: 'Name',
     *      id: 'OpA',
     *      imageURL: '/icon/'
     *    }
     * }
     */
    function _flattenOperationsList(list) {
        var map = {};

        if (!list || !list.operations) {
            debug('> App list is invalid');
            return map;
        }
        var operations = list.operations;
        Object.keys(operations).forEach(function onKey(key) {
            _mapOperation(map, key, operations[key], null, null, key, operations[key]);
        });

        return map;
    }

    /**
     * Checks the differences between the local data and the new data from server.
     * If there's something different, it fires an event to warn the controller.
     * @parameter serverMap Information from server mapped in a json object.
     */
    function _checkForDiff(localMap, serverMap) {
        var eventData = {};

        // compare new map to local stored data
        for (var serverOperationID in serverMap) {
            debug('checking differences...');
            var oldOperation = localMap[serverOperationID];
            var newOperation = serverMap[serverOperationID];

            if (!oldOperation) {
                debug('>>>> new op found');
                // new operation found
                eventData = {
                    'type': 'new',
                    'opId': serverOperationID,
                    'operation': newOperation
                };

                var evNew = document.createEvent('CustomEvent');
                evNew.initCustomEvent('operationChanged', true, true, eventData);
                document.dispatchEvent(evNew);
                break;
            } else {
                if (!_isEqual(newOperation, oldOperation)) {
                    debug('>>>> changes found in ' + serverOperationID);
                    // changes in operation
                    eventData = {
                        'type': 'update',
                        'opId': serverOperationID,
                        'operation': newOperation
                    };

                    var evtChange = document.createEvent('CustomEvent');
                    evtChange.initCustomEvent('operationChanged', true, true, eventData);
                    document.dispatchEvent(evtChange);
                    break;
                }
            }
        }
        for (var localOperationID in localMap) {
            var operation = serverMap[localOperationID];
            if (!operation) {
                debug('removed found');
                eventData = {
                    'type': 'remove',
                    'opId': localOperationID,
                    'operation': localMap[localOperationID]
                };

                var evtDeleted = document.createEvent('CustomEvent');
                evtDeleted.initCustomEvent('operationChanged', true, true, eventData);
                document.dispatchEvent(evtDeleted);
                break;
            }
        }
    }

    /**
     *  Tries to authenticate the user
     *  @parameter user Username to be authenticated
     *  @parameter pswd Password for the username
     *  @parameter callback To be called when action is done with response/error
     */
    function authenticate(user, pswd, callback) {
        debug('> PROXY authenticate');

        if (!_isOnline()) {
            debug('is online');
            callback(ErrorManager.errorList.offline);
            return;
        }

        var data = 'username=' + encodeURIComponent(user) + '&password=' + encodeURIComponent(pswd);

        API.authenticate(data, function onResponse(error, data) {
            if (!error || error.code === 106) {
                API.setAuthCookie(data);
                asyncStorage.setItem(KEY_AUTH_COOKIE, data, function () {
                    asyncStorage.setItem(KEY_USER_EMAIL, user, function () {
                        asyncStorage.setItem(KEY_LAST_LOGIN, new Date().toDateString(), function () {
                            debug('> PROXY cookie, email and last login stored');
                        });
                    });
                });
            }

            callback(error, data);
        });
    }

    function getAuthDetails(id, callback) {
        API.getAuthDetails(id, function (err, data) {

            callback(err, data);
        });
    }

    function getUserEmail(callback) {
        asyncStorage.getItem(KEY_USER_EMAIL, function (data) {
            debug('> PROXY > KEY_USER_EMAIL = ' + data);
            callback(data);
        });
    }

    function getLastLogin(callback) {
        asyncStorage.getItem(KEY_LAST_LOGIN, function (data) {
            debug('> PROXY > KEY_LAST_LOGIN = ' + data);
            callback(data);
        });
    }

    function getOrangeRibbonClosed(callback) {
        asyncStorage.getItem(KEY_ORANGE_RIBBON, function (data) {
            debug('> PROXY > KEY_ORANGE_RIBBON = ' + data);
            callback(data);
        });
    }

    function setOrangeRibbonClosed(value) {
        asyncStorage.setItem(KEY_ORANGE_RIBBON, value);
    }

    /**
     *  Asks for a new pairing token
     *  @parameter callback To be called when action is done with response/error
     */
    function getPairingToken(callback) {
        debug('> PROXY get pairing token');

        if (!_isOnline()) {
            callback(ErrorManager.errorList.offline);
            return;
        }

        API.pairingToken(function retrieved(error, tokenJSON) {
            callback(error, tokenJSON);
        });
    }

    function getLogEntries(from, to, opId, callback) {
        debug('> PROXY get log');

        if (!_isOnline()) {
            callback(ErrorManager.errorList.offline);
            return;
        }

        API.log(from.getTime(), to.getTime(), opId, function retrieved(error, tokenJSON) {
            callback(error, tokenJSON);
        });
    }

    /**
     * Utility function in charge of mapping an operation and it's subtree
     */
    function _mapOperation(map, opId, operation, parentId, parent,
            globalParentId, globalParent) {

        // Add extra information and add to the plain map
        operation.parent = parent;
        operation.parentId = parentId;
        operation.globalParent = globalParent;
        operation.globalParentId = globalParentId;

        map[opId] = operation;

        if (operation.operations && operation.operations.length !== 0) {
            for (var childOpId in operation.operations) {

                _mapOperation(map, childOpId, operation.operations[childOpId],
                        opId, operation, globalParentId, globalParent);
            }
        }
    }

    /**
     * Retrieves the preferences from server and updates the local copy
     */
    function getPreferences(callback) {
        if (typeof callback !== 'function') {
            callback = function () {
            };
        }

        asyncStorage.getItem(KEY_PREFERENCES_LIST, function (localListPreferences) {
            if (!localListPreferences) {
                localListPreferences = {};
            }
            API.preferences(function (error, preferencesServerList) {
                preferencesMap = preferencesServerList;

                debug("Preferences in server: " + JSON.stringify(preferencesServerList));

                //If there are no preferences, set default preferences
                if (Object.keys(preferencesServerList).length === 0) {
                    preferencesServerList["unlockedNotifications"] = true;
                    preferencesServerList["soundNotifications"] = true;
                    preferencesServerList["passwordAskingTime"] = -1;
                    preferencesServerList["autolock"] = 5;

                    debug("new Preferences: " + JSON.stringify(preferencesServerList));

                    storePreferences(preferencesServerList);
                }

                asyncStorage.setItem(KEY_PREFERENCES_LIST, preferencesServerList);

                debug("PROXY > getPreferences > preferences = " + JSON.stringify(preferencesServerList));

                callback(null, preferencesServerList);

            });
        });
    }

    function storePreferences(preferences) {
        API.storePreferences(preferences, function (err, data) {
            if (err) {
                ErrorManager.checkError(err);
                return;
            }
        });
    }

    function storeLogFilter(filter, callback) {
        debug('PROXY > storing log filter: ' + filter);
        asyncStorage.setItem(KEY_LOG_FILTER_TYPES, filter, callback);
    }

    function retrieveLogFilter(callback) {
        debug('PROXY > retrieving log filter');
        asyncStorage.getItem(KEY_LOG_FILTER_TYPES, callback);
    }

    /**
     *  Wrapper for the getApplications function.
     *  - With opId, will return the specific operation from the mapped tree.
     *  - Without it (null), will return the entire list from server.
     *  @parameter opId Identifier for the operation to retrieve [optional]
     *  @parameter callback To return the data. Format (error, data)
     */
    function getOperations(opId, callback, forceUpdate) {
        if (typeof callback !== 'function') {
            callback = function () {
            };
        }
        if (!opId) {
            // First cached apps
            asyncStorage.getItem(KEY_APPS_LIST, function (localList) {
                if (!localList) {
                    localList = {};
                }
                if (!forceUpdate) {
                    _flattenOperationsList(localList);
                    callback(null, localList);
                    return;
                }
                // Now fetch from the API
                API.applications(function listReceived(error, serverList) {

                    // Merge operations and externallyUpdated objects from server
                    serverList = _setExternalUpdatesFlag(serverList);

                    // Add groupId attribute to those apps belonging to a group
                    _addGroupIdLabel(serverList);

                    // flatten data from list to temporal map
                    var serverMap = _flattenOperationsList(serverList);
                    // copy local to temp to avoid race condition when comparing
                    var localMap = operationsMap;

                    // update the old data with the new
                    operationsMap = serverMap;
                    operationsRawData = serverList;
                    callback(null, serverList);
                    asyncStorage.setItem(KEY_APPS_LIST, serverList);
                    if (forceUpdate) {
                        _checkForDiff(localMap, serverMap);
                    }
                });
            });
        } else {
            if (forceUpdate) {
                API.applications(function listReceived(error, serverList) {

                    // Merge operations and externallyUpdated objects from server
                    serverList = _setExternalUpdatesFlag(serverList);

                    // Add groupId attribute to those apps belonging to a group
                    _addGroupIdLabel(serverList);

                    // flatten data from list to temporal map
                    var serverMap = _flattenOperationsList(serverList);
                    // copy local to temp to avoid race condition when comparing
                    var localMap = operationsMap;

                    // update the old data with the new
                    operationsMap = serverMap;
                    operationsRawData = serverList;
                    callback(null, operationsMap[opId]);
                    asyncStorage.setItem(KEY_APPS_LIST, serverList);
                    if (forceUpdate) {
                        _checkForDiff(localMap, serverMap);
                    }
                });
            }
            if (typeof callback === 'function') {
                callback(null, operationsMap[opId]);
            }
        }
    }


    function _addGroupIdLabel(data) {

        if (data && data.tags) {

            Object.keys(data.tags).forEach(function (groupId) {
                var currentGroup = data.tags[groupId];
                data.operations[groupId] = currentGroup;

                for (var i = 0; i < currentGroup.apps.length; i++) {
                    var opid = currentGroup.apps[i];
                    data.operations[opid].groupId = groupId;
                }
            });
        }
    }

    /**
     * Private method to add a flag to those operations that have been externally updated
     * @param {type} data
     * @returns {an operations collection where some operations may have an operation.externallyUpdated boolean flag}
     */
    function _setExternalUpdatesFlag(data) {
        Object.keys(data.operations).forEach(function (opId) {
            var operation = data.operations[opId];
            _recursivelyApplyExternalUpdatesFlag(opId, operation, data);
        });

        return data;
    }

    function _recursivelyApplyExternalUpdatesFlag(opId, operation, data) {

        var updated = data.externalUpdates && data.externalUpdates.operations && data.externalUpdates.operations[opId] && data.externalUpdates.operations[opId].status;
        if (updated !== undefined) {
            operation.externallyUpdated = updated;
        }

        // Update children, if any
        if (operation.operations) {
            Object.keys(operation.operations).forEach(function (subopID) {
                var subOperation = operation.operations[subopID];
                _recursivelyApplyExternalUpdatesFlag(subopID, subOperation, data);
            });
        }

    }
    /**
     * This function returns the latchon operation
     */
    function getLocalLatchon() {
        var op = operationsMap[Config.environment.latchonID];
        return op;
    }
    /**
     * Utility function that only return the latest data stored in local
     */
    function getLocalData() {
        return operationsRawData;
    }

    function _checkUpdateOperationRequirements(id, callback) {

        if (!_isOnline()) {
            callback(ErrorManager.errorList.offline);
            return false;
        }

        if (!id) {
            console.error('Missing \'ID\' parameter');
            callback(ErrorManager.errorList.parameter);
            return false;
        }
        return true;
    }

    function _performUpdateApiCall(data, backupOperation, callback) {
        var formattedData = data.join('&');
        API.status(formattedData, function updated(error, response) {
            if (error && backupOperation) {
                _rollbackLocalMap(backupOperation.opId, backupOperation);
            }
            callback(error, response);
        });
    }

    function multiUpdate(id, updates, callback) {

        function updateLocalOperation(field, value, operation) {
            switch (field) {
                case 'status' :
                    updateLocalOperationStatus([id], value);
                    break;
                case 'from' :
                    updateLocalOperationSchedulingRange([id], value, operation.to);
                    break;
                case 'to' :
                    updateLocalOperationSchedulingRange([id], operation.from, value);
                    break;
                case 'two_factor' :
                    updateLocalOperationTwoFactor([id], value);
                    break;
                case 'lock_on_request' :
                    updateLocalOperationAutoCloseByUse([id], value);
                    break;
                case 'autoclose' :
                    updateLocalOperationAutoClose([id], value);
                    break;
                case 'customName' :
                    updateLocalOperationName([id], value);
                    break;
            }
        }

        var operation = operationsMap[id];
        if (operation) {
            var backupOperation = _nonRecursiveOperationBackup(operation);
        }

        var data = [];
        for (var i = 0; i < updates.length; i++) {
            var currentUpdate = updates[i];

            var field = currentUpdate.field;
            var value = currentUpdate.value;

            if (field === undefined || value === undefined) {
                callback(ErrorManager.errorList.parameter);
                return;
            }

            data.push(field + '[' + id + ']=' + value);
            updateLocalOperation(field, value, operation);
        }

        _performUpdateApiCall(data, backupOperation, callback);

    }


    function updateStatus(id, status, callback) {

        var conditionsSatisified = _checkUpdateOperationRequirements(id, callback);
        if (!conditionsSatisified) {
            return;
        }

        var operation = operationsMap[id];

        if (operation) {
            var backupOperation = _nonRecursiveOperationBackup(operation);
        }

        // Check if the existing status is correct
        if (['on', 'off', 'interval'].indexOf(status) > -1) {
            var data = ['status[' + id + ']=' + status];
        } else {
            console.error('Wrong STATUS parameter');
            callback(ErrorManager.errorList.parameter);
            return;
        }

        updateLocalOperationStatus([id], status);

        _performUpdateApiCall(data, backupOperation, callback);
    }

    function updateSchedulingRange(id, from, to, callback) {

        var conditionsSatisified = _checkUpdateOperationRequirements(id, callback);
        if (!conditionsSatisified) {
            return;
        }

        var data = [];
        var operation = operationsMap[id];

        if (operation) {
            var backupOperation = _nonRecursiveOperationBackup(operation);
        }

        data.push('from[' + id + ']=' + from);
        data.push('to[' + id + ']=' + to);

        updateLocalOperationSchedulingRange([id], from, to);

        _performUpdateApiCall(data, backupOperation, callback);
    }

    function updateTwoFactor(id, twoFactor, callback) {

        var conditionsSatisified = _checkUpdateOperationRequirements(id, callback);
        if (!conditionsSatisified) {
            return;
        }

        var operation = operationsMap[id];

        if (operation) {
            var backupOperation = _nonRecursiveOperationBackup(operation);
        }

        if (['on', 'off'].indexOf(twoFactor) > -1) {
            var data = ['two_factor[' + id + ']=' + twoFactor];
        } else {
            console.error('Wrong TWO FACTOR parameter');
            callback(ErrorManager.errorList.parameter);
            return;
        }

        updateLocalOperationTwoFactor([id], twoFactor);

        _performUpdateApiCall(data, backupOperation, callback);
    }

    function updateAutoClose(id, autoClose, callback) {

        var conditionsSatisified = _checkUpdateOperationRequirements(id, callback);
        if (!conditionsSatisified) {
            return;
        }

        var operation = operationsMap[id];

        if (operation) {
            var backupOperation = _nonRecursiveOperationBackup(operation);
        }

        if (autoClose || autoClose === 0) { // 0 is possible value for autoclose
            var data = ['autoclose[' + id + ']=' + autoClose];
        }

        updateLocalOperationAutoClose([id], autoClose);

        _performUpdateApiCall(data, backupOperation, callback);
    }

    function updateAutoCloseByUse(id, autoCloseByUse, callback) {

        var conditionsSatisified = _checkUpdateOperationRequirements(id, callback);
        if (!conditionsSatisified) {
            return;
        }

        var operation = operationsMap[id];

        if (operation) {
            var backupOperation = _nonRecursiveOperationBackup(operation);
        }

        if (autoCloseByUse) {
            var data = ['lock_on_request[' + id + ']=' + autoCloseByUse];
        }

        updateLocalOperationAutoCloseByUse([id], autoCloseByUse);

        _performUpdateApiCall(data, backupOperation, callback);
    }

    function updateName(id, name, callback) {

        var conditionsSatisified = _checkUpdateOperationRequirements(id, callback);
        if (!conditionsSatisified) {
            return;
        }

        var operation = operationsMap[id];

        if (operation) {
            var backupOperation = _nonRecursiveOperationBackup(operation);
        }

        var data = ['customName[' + id + ']=' + name];

        updateLocalOperationName([id], name);

        _performUpdateApiCall(data, backupOperation, callback);
    }

    function skipPush(id, skipPush, callback) {

        var conditionsSatisified = _checkUpdateOperationRequirements(id, callback);
        if (!conditionsSatisified) {
            return;
        }

        var operation = operationsMap[id];

        if (operation) {
            var backupOperation = _nonRecursiveOperationBackup(operation);
        }

        var data = ['skipPush[' + id + ']=' + skipPush];

        updateLocalOperationSkipPush([id], skipPush);

        _performUpdateApiCall(data, backupOperation, callback);
    }

    function addAppToGroup(appId, groupId, callback) {
        debug('PROXY > addAppToGroup');

        var data = 'additions[' + groupId + ']=' + appId;

        API.addAppToGroup(data, function (err) {
            callback(err);
        });
    }

    function removeAppFromGroup(appId, groupId, callback) {
        debug('PROXY > removeAppFromGroup');

        var data = 'removals[' + groupId + ']=' + appId;

        API.removeAppFromGroup(data, function (err) {
            callback(err);
        });
    }

    function createGroup(name, opId, callback) {

        debug('PROXY > createGroup');

        var data = 'groupName=' + name;

        if (opId) {
            data += '&appIds[]=' + opId;
        }

        API.createGroup(data, function (err, data) {
            callback(err, data);
        });
    }

    function deleteGroup(groupId, callback) {

        debug('PROXY > deleteGroup');

        API.deleteGroup(groupId, function (err) {
            callback(err);
        });
    }



    function _rollbackLocalMap(opId, operation) {
        // Rollback in memory map to previous operation state
        /*jshint camelcase: false */
        updateLocalOperationFull([opId],
                operation.status,
                operation.from,
                operation.to,
                operation.two_factor,
                operation.autoclose,
                operation.lock_on_request,
                operation.customName,
                operation.skipPush);

        // Send event to update any view (if current view needs update)
        var eventData = {
            'type': 'update',
            'opId': opId,
            'operation': operation
        };

        var evtChange = document.createEvent('CustomEvent');
        evtChange.initCustomEvent('operationChanged', true, true, eventData);
        document.dispatchEvent(evtChange);
    }

    /**
     * Update the memory cache of the operation, this is being called
     */
    function updateLocalOperationFull(operationList, status, from, to, twoFactor, autoclose, autocloseByUse, customName, skipPush) {
        operationList.forEach(function (opId) {
            if (operationsMap) {
                /*jshint camelcase: false */
                // Change the operation itself

                var op = operationsMap[opId];
                if (!op) {
                    op = new Object();
                    operationsMap[opId] = op;
                }
                if (status) {
                    op.status = status;
                }
                if (from) {
                    op.from = from;
                }
                if (to) {
                    op.to = to;
                }
                if (twoFactor) {
                    op.two_factor = twoFactor;
                }
                if (autoclose) {
                    op.autoclose = autoclose;
                }
                if (autocloseByUse) {
                    op.lock_on_request = autocloseByUse;
                }
                if (customName) {
                    op.customName = customName;
                }
                if (skipPush) {
                    op.skipPush = skipPush;
                }
            }

            // Change parent child operations (as is a reference to this op)
            var opParent = operationsMap[op.parent];
            if (!opParent) {
//                debug('There is no parent to update');
                return;
            }
            if (status) {
                opParent.operations[opId].status = status;
            }
            if (from) {
                opParent.operations[opId].from = from;
            }
            if (to) {
                opParent.operations[opId].to = to;
            }
            if (twoFactor) {
                opParent.operations[opId].two_factor = twoFactor;
            }
            if (autoclose) {
                opParent.operations[opId].autoclose = autoclose;
            }
            if (autocloseByUse) {
                opParent.operations[opId].lock_on_request = autocloseByUse;
            }
            if (customName) {
                opParent.operations[opId].customName = customName;
            }
            if (skipPush) {
                opParent.operations[opId].skipPush = skipPush;
            }
        });
    }

    function updateLocalOperationStatus(operationList, status) {

        operationList.forEach(function (opId) {
            if (operationsMap) {

                var currentOperation = _getLocalOperationOrCreate(opId);

                delete currentOperation.externallyUpdated;
                currentOperation.status = status;

            }

            var parentOfCurrentOperation = operationsMap[currentOperation.parent];
            if (!parentOfCurrentOperation) {
                return;
            }

            parentOfCurrentOperation.operations[opId].status = status;
            delete parentOfCurrentOperation.operations[opId].externallyUpdated;
        });
    }
    function _getLocalOperationOrCreate(opId) {
        var currentOperation = operationsMap[opId];
        if (!currentOperation) {
            currentOperation = new Object();
            operationsMap[opId] = currentOperation;
        }

        return currentOperation;
    }

    function updateLocalOperationSchedulingRange(operationList, from, to) {

        operationList.forEach(function (opId) {
            if (operationsMap) {

                var currentOperation = _getLocalOperationOrCreate(opId);

                currentOperation.from = from;
                currentOperation.to = to;

            }

            var parentOfCurrentOperation = operationsMap[currentOperation.parent];
            if (!parentOfCurrentOperation) {
                return;
            }

            parentOfCurrentOperation.operations[opId].from = from;
            parentOfCurrentOperation.operations[opId].to = to;

        });
    }
    function updateLocalOperationTwoFactor(operationList, twoFactor) {

        operationList.forEach(function (opId) {
            if (operationsMap) {

                var currentOperation = _getLocalOperationOrCreate(opId);
                currentOperation.twoFactor = twoFactor;

            }

            var parentOfCurrentOperation = operationsMap[currentOperation.parent];
            if (!parentOfCurrentOperation) {
                return;
            }

            parentOfCurrentOperation.operations[opId].twoFactor = twoFactor;

        });
    }
    function updateLocalOperationAutoClose(operationList, autoClose) {

        operationList.forEach(function (opId) {
            if (operationsMap) {

                var currentOperation = _getLocalOperationOrCreate(opId);
                currentOperation.autoClose = autoClose;

            }

            var parentOfCurrentOperation = operationsMap[currentOperation.parent];
            if (!parentOfCurrentOperation) {
                return;
            }

            parentOfCurrentOperation.operations[opId].autoClose = autoClose;

        });
    }
    function updateLocalOperationAutoCloseByUse(operationList, autoCloseByUse) {

        operationList.forEach(function (opId) {
            if (operationsMap) {

                var currentOperation = _getLocalOperationOrCreate(opId);
                currentOperation.lock_on_request = autoCloseByUse;

            }

            var parentOfCurrentOperation = operationsMap[currentOperation.parent];
            if (!parentOfCurrentOperation) {
                return;
            }

            parentOfCurrentOperation.operations[opId].lock_on_request = autoCloseByUse;

        });
    }
    function updateLocalOperationName(operationList, customName) {

        operationList.forEach(function (opId) {
            if (operationsMap) {

                var currentOperation = _getLocalOperationOrCreate(opId);
                currentOperation.customName = customName;

            }

            var parentOfCurrentOperation = operationsMap[currentOperation.parent];
            if (!parentOfCurrentOperation) {
                return;
            }

            parentOfCurrentOperation.operations[opId].customName = customName;

        });
    }

    function updateLocalOperationSkipPush(operationList, skipPush) {

        operationList.forEach(function (opId) {
            if (operationsMap) {

                var currentOperation = _getLocalOperationOrCreate(opId);
                currentOperation.skipPush = skipPush;

            }

            var parentOfCurrentOperation = operationsMap[currentOperation.parent];
            if (!parentOfCurrentOperation) {
                return;
            }

            parentOfCurrentOperation.operations[opId].skipPush = skipPush;

        });
    }

    /**
     *  Asks for a new second factor authentication token
     *  @parameter id Identificator of the operation
     *  @parameter callback To be called when action is done with response/error
     */
    function getSecFactor(id, callback) {
        debug('> PROXY 2FactorAuth');

        if (!_isOnline()) {
            callback(ErrorManager.errorList.offline);
            return;
        }

        API.twoFactorToken(id, function retrieved(error, tokenJSON) {
            callback(error, tokenJSON);
        });
    }

    /**
     * Cleans all the current user's cookies, tokens and data locally stored.
     * Also sends, if possible, an API call to stop the notifications.
     */
    function logout(cb) {
        debug('> PROXY logout');

        // Delete cached operations information in memory
        operationsMap = null;
        operationsRawData = null;

        API.logout(function loggedOut(err) {
            API.setAuthCookie(null);

            // Remove local data, no matter if the op end up with error
            asyncStorage.removeItem(KEY_AUTH_COOKIE, function done() {
                debug('> Logged out: auth cookie deleted');

                asyncStorage.removeItem(KEY_APPS_LIST, function done() {
                    debug('> Logged out: user data deleted');

                    asyncStorage.removeItem(KEY_PREFERENCES_LIST, function done() {
                        debug('> Logged out: user settings deleted');

                        asyncStorage.removeItem(KEY_USER_EMAIL, function done() {
                            debug('> Logged out: user email deleted');

                            asyncStorage.removeItem(KEY_LAST_LOGIN, function done() {
                                debug('> Logged out: last login deleted');

                                asyncStorage.removeItem(KEY_ORANGE_RIBBON, function done() {
                                    debug('> Logged out: orange ribbon deleted');

                                    // Just execute the callback if the logout went well
                                    if (typeof cb === 'function' && !err) {
                                        cb();
                                    }
                                });
                            });
                        });
                    });
                });
            });
        });
    }

    /**
     * Utility function to update the status of multiple operations in one call
     * @parameter list Array with the different operations to update
     * @parameter status New status to update the operations with
     * @parameter callback To be called after the API call is done
     */
    function updateMultiStatus(list, status, callback) {
        if (!_isOnline()) {
            callback(ErrorManager.errorList.offline);
            return;
        }

        if (!list || !list.length) {
            callback(ErrorManager.errorList.parameter);
            return;
        }

        var data = 'status[' +
                list.join(']=' + status + '&status[') +
                ']=' + status;

        var backup = {};
        list.forEach(function onOp(operationId) {
            backup[operationId] = operationsMap[operationId].status;
        });

        updateLocalOperationStatus(list, status);
        API.status(data, function updated(error) {
            callback(error); // don't need to return the data
            if (error) {
                // Restore the original values for each op
                Object.keys(backup).forEach(function onOp(opId) {
                    updateLocalOperationStatus(opId, backup[opId]);
                });
            }
        });
    }
    /**
     * Changes the locked state of all the displayed services
     * @parameter operations Collection with the operation to be modified.
     * @parameter status The new status for the operations
     * @parameter callback To call when the change is done, or error detected
     */
    function updateLockAllOperations(status, parent, callback) {
        // If no parent declared, we assume main screen, so change the main services
        var operations = parent && parent.operations ||
                operationsRawData.operations;

        updateMultiStatus(Object.keys(operations), status, function (err) {
            callback(err);
        });
    }

    function updateOrder(opIdList) {
        var data = [];
        for (var i = 0; i < opIdList.length; i++) {
            var id = opIdList[i];
            data.push('order[' + id + ']=' + (i));
        }
        _performUpdateApiCall(data, null, function (error) {
            if (error) {
                callback(error);
            }
        });
    }

    /**
     * Returns a new object with the same attributes that operation.
     * Referenced objects such as parent and children still reference the 
     * original object.
     */
    function _nonRecursiveOperationBackup(operation) {

        var backupGlobalParent = operation.globalParent;
        var backupParent = operation.parent;
        var backupOperations = operation.operations;
        operation.globalParent = undefined;
        operation.parent = undefined;
        operation.operations = undefined;

        var backupOperation = JSON.parse(JSON.stringify(operation));

        backupOperation.globalParent = backupGlobalParent;
        backupOperation.parent = backupParent;
        backupOperation.operations = backupOperations;
        operation.globalParent = backupGlobalParent;
        operation.parent = backupParent;
        operation.operations = backupOperations;

        return backupOperation;
    }


    return {
        init: initialize,
        authenticate: authenticate,
        getAuthDetails: getAuthDetails,
        getPairingToken: getPairingToken,
        getLocalLatchon: getLocalLatchon,
        getOperations: getOperations, // We will use this one with or without params
        getLocalData: getLocalData,
        getSecondFactorAuth: getSecFactor,
        updateStatus: updateStatus,
        updateSchedulingRange: updateSchedulingRange,
        updateTwoFactor: updateTwoFactor,
        updateAutoClose: updateAutoClose,
        updateAutoCloseByUse: updateAutoCloseByUse,
        updateName: updateName,
        skipPush: skipPush,
        logout: logout,
        updateLockAllOperations: updateLockAllOperations,
        getPreferences: getPreferences,
        storePreferences: storePreferences,
        getUserEmail: getUserEmail,
        getLastLogin: getLastLogin,
        updateOrder: updateOrder,
        multiUpdate: multiUpdate,
        createGroup: createGroup,
        deleteGroup: deleteGroup,
        addAppToGroup: addAppToGroup,
        removeAppFromGroup: removeAppFromGroup,
        storeLogFilter: storeLogFilter,
        retrieveLogFilter: retrieveLogFilter,
        getLogEntries: getLogEntries,
        getOrangeRibbonClosed: getOrangeRibbonClosed,
        setOrangeRibbonClosed: setOrangeRibbonClosed,
        KEY_AUTH_COOKIE: KEY_AUTH_COOKIE
    };
}
var Proxy = new Proxy();
