'use strict';

var q = require("q");

var DistributedLock = require('../../lock/DistributedLock');
var ServerAccessManager = require('../utils/ServerAccessManager');
var documentUpdater = require('../../../shared/data/DocumentUpdater');
var templateValidator = require('../../validation/TemplateValidator');
var ruleEngine = require('../../ruleengine/RuleEngine');
var DataServiceUtil = require('./../utils/DataServiceUtil');
var UtilError = require('./../../../shared/utils/UtilError');
var UtilDocumentId = require('../../../shared/utils/UtilDocumentId');
var ServiceWorkerUpdater = require('../../serviceWorker/ServiceWorkerUpdater');
var versionManager = require('../../../shared/versioning/VersionManager');
var documentPusher = require('../../push/DocumentPusher');
var ComponentCache = require('../../cache/ComponentCache');
var ConflictResolver = require('../../../shared/data/ConflictResolver');
var _ = require('lodash/lang');
var SharedConstants = require('../../../shared/constants/SharedConstants');
var logger = require('../../utils/Logger').loggers.get('db');
var findService = require('./FindService');
var templateService = require('./TemplateService');
var redisAccountService = require('../../redis/RedisAccountDataService');
var UtilDate = require('../../utils/UtilDate');
var UtilConflict = require('../../../shared/utils/UtilConflict');
const redisConfigurationService = require('../../redis/RedisConfigurationService');
var dbConfig = require('../providers/DatabaseConfig');
var ElasticIndexingService= require('../../search/ElasticIndexingService');
const UtilDocument = require('../../../shared/utils/UtilDocument');

const lodashCloneDeep = require('lodash.clonedeep');


var accountService;

const getAccountService = () => {
  if (!accountService) {
    accountService = require('./AccountService');
  }

  return accountService;
};

const deactivateIfAccount = async (document, oldDocument, userId, fullDomainUrl) => {

    if (document && document.systemHeader && document.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_ACCOUNT) {
        // deactivate account
        if (document.email !== oldDocument.email) {
            var accountService = require('../dataServices/AccountService');
            return await accountService.deActivateAccount(document.documentId, userId, fullDomainUrl);
        }
    }

    return Promise.resolve();
};

/** update document cache with newly updated document - to make sure that the version in the cache is not out of date. Not all
 * documents will be cached but we will update the cache if has a systemType that we have decided to cache
 */
function updateDocumentCache(document, oldDocument, userId) {
    if (document && document.systemHeader ) {
        if (document.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_COMPONENT) {
            if (document.systemHeader.deleted === true) {
                ComponentCache.removeComponent(oldDocument);
            } else {
                if ((document.name !== oldDocument.name)){
                    ComponentCache.removeComponent(oldDocument);
                }
                ComponentCache.setComponent(document.name, document, userId);
            }

            let vendorLibUpdated = false;

            if (document.vendorLibrariesRel) {    
                if (oldDocument.vendorLibrariesRel) {
                    for (const docLib of document.vendorLibrariesRel) {
                        if (!oldDocument.vendorLibrariesRel.find(vendLib => docLib.fileName === vendLib.fileName)) {
                            vendorLibUpdated = true;
                        }
                    }
                } else {
                    vendorLibUpdated = true;
                }
            }

            if (vendorLibUpdated) {
                ServiceWorkerUpdater.updateServiceWorker();
            }
        }

        if (document.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_ACCOUNT) {
            if (document.systemHeader.deleted === true) {
                redisAccountService.removeCachedAccount(document);
            } else {
                redisAccountService.setAccount(document);
            }
        } else if (document.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_ACCOUNT_CONTROL) {
            if (document.accountRel && document.accountRel.length > 0) {
                let accountRelDocId = document.accountRel[0].documentId;
                redisAccountService.removeAccountControl(accountRelDocId);
            }
        } else if (document.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_ACCOUNT_SECURITY) {
            if (document.accountRel && document.accountRel.length > 0) {
                let accountRelDocId = document.accountRel[0].documentId;
                redisAccountService.removeAccountSecurity(accountRelDocId);
            }
        }

        if (document.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_CONFIGURATION) {
            redisConfigurationService.setConfiguration(document);
            ServiceWorkerUpdater.updateServiceWorker();
        }
    }
}

function loadVersion(versionId, documentId, params) {
    var qry =  DBProvider.dataUpdateService.getLoadVersionQuery(versionId);
    var query = qry.query;
    var sortQuery = qry.sortQuery;

    return findService.findAll(query, params.userId, sortQuery, true, params).then(
        function successFunc(results) {

            if(results && results.length) {
                return Promise.resolve(results[0]);
            }
            return Promise.reject(new Error("No version found " + versionId + " for document: " + documentId));
        },
        function errorFunc(err) {

            return Promise.reject(
                new Error("No version found " + versionId + " for document: " + documentId + " due to err: " + err)
            );
        }
    );
}

/*
 Tailored to support issue 10049.
 */
function doUpdate(params) {

    var connectionName = params.connectionName;

    var cErr = checkUpdateParams(params);

    if(cErr){
        return Promise.reject(new Error(cErr));
    }

    var documentId = params.newDocument.documentId == null ? params.documentId : params.newDocument.documentId;
    var newDocument = params.newDocument;

    var userId = params.userId;
    var isDeepDiff = params.isDeepDiff;
    var fullDomainUrl = params.fullDomainUrl;

    var dbUser;//retrieved user object via userId
    var dbDocument;
    var currentDocument;//retrieved document object via docId
    var template;//retrieved template object

    var previousVersion;//document's clone without applied updates
    var updates;//used in preprocessor to determine which fields have been modified.
    var finalDocument;
    var finalDocumentIndexingId;

    // a lock that is used to lock the document so other processes won't be able to update the document at the same time
    var lock;

    /**
     * Container for version documents. During normal operation, this should contain a single version document.
     * But if conflicts arise as detailed in 10539, where another version is created from two unresolved documents,
     * both unresolved documents will become versions of the resolved document version.
     * the old document version and the
     * @type {Array}
     */
    var versionDocuments = [];

    return DBProvider.accountDataService.getAccountById(userId)
        .then(function onUserAccRetrieveSucc(retrievedUserObject) {
            dbUser = retrievedUserObject;

            // lock the document so other processes can't update it at the same time.
            // This needs to happen before finding the document so that an old version of the document isn't found
            // if there is another process updating the document
            return DistributedLock.lockDocument(documentId);
        }).then(function documentLocked(returnedLock) {
            lock = returnedLock;
            return checkIfDocumentExists(newDocument, connectionName);
        }).then(function documentLocked(results) {

            var options = {};
            options.ignoreDeleted = true;
            options.connectionName = connectionName;

            if (params.isUpdateFromPreviousVersion || params.importPreviousVersion) {
                var prevVersionId = newDocument.systemHeader.previousVersionId;
                return loadVersion(prevVersionId, documentId, params).then(
                    function(foundVersion) {
                        if (params.importPreviousVersion) {
                            // OK, found the previous version of the imported one. Keep it in the history
                        }
                        return Promise.resolve(foundVersion);
                    },
                    function(notFoundError) {
                        if (params.importPreviousVersion) {
                            // not found the previous version of the imported one. Let find the current one.
                            return findService.findOne(documentId, userId, options);
                        }
                        return Promise.reject(notFoundError);
                    }
                );
            }

            return findService.findOne(documentId, userId, options);
        }).then(function onDocumentRetrieveSucc(retrieved) {

            dbDocument = retrieved;

            if (dbDocument == null) {
                return Promise.reject(new Error('Couldn\'t get document ' + documentId + ' with userId ' + userId));
            }

            if (!params.isUpdateFromPreviousVersion && dbDocument.systemHeader.currentVersion !== true) {
                return Promise.reject(new Error('Should not update version documents.'));
            }

            return ServerAccessManager.checkAccess(dbDocument, dbUser, SharedConstants.OPERATION_TYPE_UPDATE);
        }).then(function onCheckAccessComplete(hasAccess) {

            if (!hasAccess) {
                return Promise.reject(UtilError.createForbiddenError(dbDocument, SharedConstants.OPERATION_TYPE_UPDATE));
            }

            var setCurrentVersion = function(currentVersion) {
                if (!newDocument.systemHeader.createdDate) {
                    newDocument.systemHeader.currentVersion = false;
                } else if (!currentVersion.systemHeader.createdDate) {
                    newDocument.systemHeader.currentVersion = true;
                } else {
                    let diff = UtilDate.compare(newDocument.systemHeader.createdDate,
                        currentVersion.systemHeader.createdDate);
                    if (diff >= 0) {
                        newDocument.systemHeader.currentVersion = true;
                    } else {
                        newDocument.systemHeader.currentVersion = false;
                    }
                }
            };

            var checkImportedPrevVersion = async function() {

                if (params.importPreviousVersion) {

                    if (!dbDocument.systemHeader.currentVersion) {
                        // Find current version to compare its created date(newest created date) with imported version
                        // to set currentVersion value to the imported one
                        var options = {};
                        options.ignoreDeleted = true;
                        options.connectionName = connectionName;

                        var currentVersion = await findService.findOne(documentId, userId, options);

                        setCurrentVersion(currentVersion);

                    } else {

                        setCurrentVersion(dbDocument);

                    }

                }

                return Promise.resolve();
            };

            if (params.makeCurrentVersion || params.importPreviousVersion) {

                // Need to check if the versionId of newDocument is a previous one
                return loadVersion(newDocument.systemHeader.versionId, documentId, params).then(
                    function(version) {

                        if (params.importPreviousVersion &&
                            newDocument.systemHeader.currentVersion === version.systemHeader.currentVersion) {

                            return Promise.reject(new Error("Do not import previous version because of " +
                                "matching versionId and currentVersion."));

                        }

                        return Promise.resolve(version);
                    },
                    function(err) {

                        // There has no existing version with that versionId from newDocument
                        return checkImportedPrevVersion();

                    }
                );
            }

            return Promise.resolve();

        }).then(function(prevDocument) {

            if (!params.isUpdateFromPreviousVersion) {
                // the _id needs to be read before the document is cloned.
                // The cloning of the object makes the toHexString function fail
                finalDocumentIndexingId = DBProvider.dataUpdateService.getIndexId(dbDocument);

                if (!newDocument.systemHeader) {
                    newDocument.systemHeader = {};
                    newDocument.systemHeader.versionId = UtilDocumentId.generateId();
                    newDocument.systemHeader.previousVersionId = dbDocument.systemHeader.versionId;
                }

                if (dbDocument.systemHeader.versionId === newDocument.systemHeader.versionId) {
                    newDocument.systemHeader.versionId = UtilDocumentId.generateId();
                    newDocument.systemHeader.previousVersionId = dbDocument.systemHeader.versionId;
                }

                if (params.makeCurrentVersion) {
                    // The provided versionId of newDocument is the previous one, let create a new versionId
                    if (prevDocument) {
                        newDocument.systemHeader.versionId = UtilDocumentId.generateId();
                    }
                    newDocument.systemHeader.previousVersionId = dbDocument.systemHeader.versionId;
                }
            }

            if (!newDocument.systemHeader.versionId) {
                newDocument.systemHeader.versionId = UtilDocumentId.generateId();
            }

            currentDocument = dbDocument;

            if (!params.isUpdateFromPreviousVersion && (!params.importPreviousVersion ||
                (params.importPreviousVersion && newDocument.systemHeader.currentVersion))) {
                //once we apply updates and create the finalDocument this document will become an old version
                versionDocuments.push(currentDocument);
            }

            //by default (without conflicts), the current version will always become the previousVersion.
            // But in cases where a conflict arise, the newest document according to createdDate (which is set by the client)
            // becomes the previousVersion.
            previousVersion = currentDocument;//used writeSystemHeader to set the finalDocument's previousVersion

            //Our final document applied with all changes. This will be mutated when applyUpdatesFn is called:
            if (params.makeCurrentVersion || params.importPreviousVersion) {
                finalDocument = lodashCloneDeep(newDocument, true);
            } else {
                finalDocument = lodashCloneDeep(currentDocument, true);
            }

            if (params.isUpdateFromPreviousVersion) {
                finalDocument.systemHeader.currentVersion = true;
            }

            if (!params.updates && !params.makeCurrentVersion && !params.importPreviousVersion) {
                // In case no updates provided, load the updates from prev version.
                // If no prev one found, load the updates from current one
                return loadVersion(params.newDocument.systemHeader.previousVersionId, documentId, params).then(
                    function foundPrevFunc(prevVersion) {
                        params.updates = documentUpdater.getUpdates(prevVersion, params.newDocument);
                        return findTemplate();
                    },
                    function notFoundPreVer(err) {
                        params.updates = documentUpdater.getUpdates(currentDocument, params.newDocument);
                        return findTemplate();
                    }
                );
            }

            function findTemplate() {
                var withTemplate = (dbDocument.systemHeader && dbDocument.systemHeader.templateId);
                if(!withTemplate){
                    template = null;
                    return Promise.resolve(template);//next handler expects a template document
                }

                var templateId = params.templateId != null ? params.templateId : dbDocument.systemHeader.templateId;

                return templateService.getTemplate(templateId, connectionName);
            }

            return findTemplate();

        }).then(function onTemplateRetrieveSucc(retrievedTemplate){

            if (retrievedTemplate != null) {
                template = retrievedTemplate;
            }

            if (params.isUpdateFromPreviousVersion) {
                //load and set current version in DB to false
                var options = {
                    ignoreDeleted: true,
                    connectionName: connectionName
                };
                return findService.findOne(documentId, userId, options).then(
                    function successFunc(latestCurrentVersion) {
                        if (latestCurrentVersion) {
                            latestCurrentVersion.systemHeader.currentVersion = false;
                            versionDocuments.push(latestCurrentVersion);
                            finalDocumentIndexingId = DBProvider.dataUpdateService.getIndexId(latestCurrentVersion);
                        }

                        return doApplyUpdate();
                    },
                    function errorFunc(error) {
                        return Promise.reject(error);
                    }
                );
            }

            if (!params.makeCurrentVersion && !params.importPreviousVersion) {
                var newDocPrevVersionId = newDocument.systemHeader != null ? newDocument.systemHeader.previousVersionId : null;
                if (newDocPrevVersionId) {

                    return loadVersion(newDocPrevVersionId, documentId, params).then(
                        function successFunc(newDocPrevVersion) {

                            var mayConflict = newDocPrevVersionId && newDocPrevVersionId !== currentDocument.systemHeader.versionId;
                            if (mayConflict) {

                                //save as a new version into DB for the incoming version in case having conflicting or not
                                //this will not cause version lost.
                                var now = Date.now();
                                DataServiceUtil.writeSystemHeader(
                                    newDocument,
                                    SharedConstants.INSERT_OPERATION_TYPE,
                                    dbUser,
                                    template,
                                    newDocPrevVersion,
                                    updates,
                                    now
                                );
                                versionDocuments.push(newDocument);

                                var fromCurrentUpdates = documentUpdater.getUpdates(newDocPrevVersion, currentDocument);
                                var isConflict = UtilConflict.checkConflict(params.updates, fromCurrentUpdates);
                                if (isConflict) {

                                    var resolver = isDeepDiff === true ?
                                        ConflictResolver.resolveJSONPatch : ConflictResolver.resolveSimple;

                                    var conflictResolverParams = {
                                        userId: userId,
                                        newDocument: newDocument,
                                        newDocumentBaseDoc: newDocPrevVersion,
                                        currentDocument: currentDocument,
                                        newUpdates: params.updates, //this updates should be pushed on client side
                                        oldUpdates: fromCurrentUpdates,
                                        isDeepDiff: isDeepDiff
                                    };

                                    var results = resolver(conflictResolverParams);
                                    if (results != null) {

                                        previousVersion = results.previousVersion;
                                        finalDocument = results.finalDoc;
                                    }
                                } else {
                                    var compared = UtilDate.compare(currentDocument.systemHeader.createdDate,
                                        newDocument.systemHeader.createdDate);

                                    //merge changes on no conflict fields
                                    if (compared <= 0) {
                                        documentUpdater.applyUpdates(finalDocument, fromCurrentUpdates);
                                        documentUpdater.applyUpdates(finalDocument, params.updates);
                                        previousVersion = newDocument;
                                    } else {
                                        documentUpdater.applyUpdates(finalDocument, params.updates);
                                        documentUpdater.applyUpdates(finalDocument, fromCurrentUpdates);
                                        previousVersion = currentDocument;
                                    }

                                    finalDocument.systemHeader.currentVersion = true;
                                }

                                // the final doc is a new version so we generate information for it
                                // set createdDate here to make it before serverCreatedDate/serverUpdatedDate in the next
                                // step of writeSystemHeader.
                                finalDocument.systemHeader.createdDate = now;
                                // update prev versionId here from correct prev version instead of writeSystemHeader
                                // because it does not deal with conflict
                                finalDocument.systemHeader.previousVersionId = previousVersion.systemHeader.versionId;

                                return finishUpdates();

                            } else {

                                return doApplyUpdate();
                            }
                        },
                        function errorFunc(err) {
                            //Set previousVersionId for newDocument to currentVersionId due to unexisted
                            newDocument.systemHeader.previousVersionId = currentDocument.systemHeader.versionId;
                            return doApplyUpdate();
                        }
                    );
                }
            }

            return doApplyUpdate();

            function doApplyUpdate() {
                // do not apply updates in case make the new document current version
                if (!params.makeCurrentVersion && !params.importPreviousVersion) {
                    var applyUpdatesFn = (isDeepDiff === true) ? documentUpdater.applyUpdatesDeepDiff :
                        documentUpdater.applyUpdatesSimple;
                    applyUpdatesFn(finalDocument, newDocument);
                }

                //delete conflicts data due to this version does not conflict with any versions
                delete finalDocument.systemHeader.conflicts;
                delete finalDocument[SharedConstants.MAKE_CURRENT_VERSION_KEY];

                return finishUpdates();
            }

            function finishUpdates() {
                var doPrepareUpdate = function() {
                    // will be used in preprocessor to determine which fields have been modified.
                    updates = documentUpdater.getUpdates(currentDocument, finalDocument);

                    if (template == null) {//skip template validation
                        return Promise.resolve();
                    }
                  
                    DataServiceUtil.setDefaultValues(finalDocument, template);

                    var operation = SharedConstants.UPDATE_OPERATION_TYPE;
                    return templateValidator.templateValidatorAndMapping(finalDocument, template, userId, operation);
                };

                // block the update if it is setting the systemHeader.deleted flag to true
                // and the account doesn't have delete access
                if ((finalDocument.systemHeader.deleted && !previousVersion.systemHeader.deleted) ||
                    (!finalDocument.systemHeader.deleted && previousVersion.systemHeader.deleted)) {
                    return ServerAccessManager.checkAccess(previousVersion, dbUser, SharedConstants.OPERATION_TYPE_DELETE)
                        .then(function(hasDeleteAccess) {
                            if (!hasDeleteAccess) {
                                return Promise.reject(UtilError.createForbiddenError(finalDocument,
                                    SharedConstants.OPERATION_TYPE_DELETE));
                            }

                            return doPrepareUpdate();
                        });
                }

                return doPrepareUpdate();
            }

        }).then(function onTemplateValidationSuccess() {

            if (template == null) {//skip pre-process
                return Promise.resolve();
            }

            //require PreProcessor only when needed, dbProvider.counterService
            // is null if placed at the top
            var PreProcessor = require('./../preprocessors/PreProcessor');

            return PreProcessor.preProcessSaveFields(
                template,
                finalDocument,
                SharedConstants.UPDATE_OPERATION_TYPE,
                updates,
                isDeepDiff,
                userId
            );

        }).then(function onPreprocessingSuccess() {
            var deferred = q.defer();

            var docContext = {
                user : dbUser
            };

            var preSaveName = SharedConstants.RULESET_RELATIONSHIPS.PRESAVE_SERVER_EVENT_NAME.name;
            var preSaveEventData = params.eventData && params.eventData[preSaveName] ? params.eventData[preSaveName]: null;
            var isPreSaveOnDoc = preSaveEventData && preSaveEventData.documentId === finalDocument.documentId;
            var shouldNotRun = isPreSaveOnDoc || (preSaveEventData && preSaveEventData.eventUpdateCount > 0 &&
              preSaveEventData.documentId === finalDocument.documentId);

            if (!preSaveEventData || shouldNotRun === false) {

                if (isPreSaveOnDoc) {
                    docContext.eventData = params.eventData;
                }
                docContext.documentSession = params.documentSession;
                docContext.isNew = false;

                ruleEngine.doPreSaveServer(finalDocument, docContext, preSaveDone);

            } else {
                logger.info("Break the presaveserver save rule due to looping prevention.");
                preSaveDone();
            }

            function preSaveDone(ruleResult) {
                var status = ruleResult ? ruleResult.status : SharedConstants.STATUS_SUCCESS;

                if(status && status.toLowerCase() === SharedConstants.STATUS_ERROR){
                    var msg = "Error in PreSaveServer rule processing: " + JSON.stringify(ruleResult);
                    logger.error(msg);
                    return deferred.reject(new Error(msg));

                } else if (finalDocument && finalDocument.systemHeader && currentDocument &&
                    currentDocument.systemHeader &&
                    finalDocument.systemHeader.templateId !== currentDocument.systemHeader.templateId) {

                    if (!finalDocument.systemHeader.templateId){
                        var errMsg = "Error in template id change: Invalid template";
                        return deferred.reject(new Error(errMsg));
                    }
                    templateService.getTemplate(finalDocument.systemHeader.templateId).then(
                        function sucFunc(template){
                            ServerAccessManager.checkAccess(template, dbUser, SharedConstants.OPERATION_TYPE_CREATE)
                                .then(
                                    function sucFunc(hasCreateAccess){
                                        systemHeaderTemplateIdChangeHandlerDone(hasCreateAccess, template);
                                    },
                                    function errFunc(err){
                                        return deferred.reject(err);
                                    }
                                );
                        },
                        function errFunc(err){
                            return deferred.reject(err);
                        }
                    );
                }
                else{
                    systemHeaderTemplateIdChangeHandlerDone(true, template);
                }

            }

            function systemHeaderTemplateIdChangeHandlerDone(hasAccess, template) {
                if (!hasAccess) {
                    var errMsg = SharedConstants.OPERATION_TYPE_CREATE + " access denied on document " +
                        template.documentId + ".  You need to have Create access to change the template id of a document.";
                    return deferred.reject(new Error(errMsg));
                }

                var now = new Date();

                DataServiceUtil.writeSystemHeader(
                    finalDocument,
                    SharedConstants.UPDATE_OPERATION_TYPE,
                    dbUser,
                    template,
                    previousVersion,
                    updates,
                    now
                );

                // generate the versionId if it doesn't exist
                if(finalDocument.systemHeader.versionId === finalDocument.systemHeader.previousVersionId){
                    finalDocument.systemHeader.versionId = UtilDocumentId.generateId();
                }

                DataServiceUtil.imposeStandardDateTypeInSystemHeader(finalDocument, currentDocument);


                /**
                 * Code section for adding version documents to the batch
                 * Note: Versioning is disabled for offline updates since the version files are created by the client and are sent
                 * to the server as separate insert requests.
                 */
                var vDocuments = [];
                var disableVersioning = (template && template.disableVersioning === true);
                if (!disableVersioning || versionDocuments.length === 0) {
                    for(var k = 0 ; k < versionDocuments.length ; k++){
                        var version = versionManager.createVersion(versionDocuments[k]);
                        version.systemHeader.serverUpdatedDate = now;

                        DataServiceUtil.imposeStandardDateTypeInSystemHeader(version);
                        version = DBProvider.dataUpdateService.setVersionDocumentId(version);
                        vDocuments.push(version);
                    }
                }

                UtilDocument.removeDisableSaveFieldValues(finalDocument, template);

                //End code section for persisting version documents
                var updateParams = {
                    documentId : documentId,
                    finalDocumentIndexingId: finalDocumentIndexingId,
                    oldDocument: currentDocument,
                    updateDocument: finalDocument,
                    versionDocuments: vDocuments,
                    connectionName: connectionName
                };
                var dsConfig = dbConfig.getDataServicesConfig(connectionName);
                ElasticIndexingService.bulkIndex(updateParams, dsConfig).
                then(function indexingSuccess() {
                    DBProvider.dataUpdateService.update(updateParams).then(function successFunc(results){

                        if (finalDocument.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_ACCOUNT_SECURITY) {

                            if (!finalDocument.password) {
                                var accountService = require("./AccountService");
                                accountService.removeAccountPasswordPlaceHolder(finalDocument, userId).then(
                                    function() {
                                        logger.info("Passworld placeholder has been removed form account document.");
                                    },
                                    function(err) {
                                        logger.warn(err);
                                    }
                                );
                            }

                        }

                        // update cache if the document is cached
                        if (!params.importPreviousVersion ||
                            (params.importPreviousVersion && finalDocument.systemHeader.currentVersion)) {
                            updateDocumentCache(finalDocument, currentDocument, userId);
                        }
                        deactivateIfAccount(finalDocument, currentDocument, userId, fullDomainUrl);

                        // unlock the document. None of the operations below this should rely on the document
                        // being locked. Post save
                        // rulesets won't need a lock because any update to the document will be separate.

                        // No new functionality should be added below the unlock that needs a lock on the document
                        if (lock) {
                            logger.info("Unlocking document after update: " + documentId);
                            DistributedLock.unlockDocument(lock);
                        }

                        // pass the document straight through to the rules as it was added to the database.
                        // There aren't any triggers or defaulting values on the database so we can just
                        // pass the document through,
                        // otherwise there could be problems of the wrong document being passed through
                        // if there was another update in between. For instance if there were two updates
                        // in quick succession, both updating
                        // values that need to be passed through to the rules. If the second update had
                        // happened by the time the rules were
                        // processed the value in the first update wouldn't get passed through to the rules.
                        // This would be a problem if for
                        // example a status field was updated twice in quick succession.

                        var postSaveName = SharedConstants.RULESET_RELATIONSHIPS.POSTSAVE_EVENT_NAME.name;
                        var isPostSaveOnDoc = params.eventData &&
                            params.eventData[postSaveName] &&
                            params.eventData[postSaveName].documentId === finalDocument.documentId;
                        var shouldNotRun = isPostSaveOnDoc && params.eventData[postSaveName].eventUpdateCount > 0;

                        if (!params.eventData || !params.eventData[postSaveName] || shouldNotRun === false) {

                            if (isPostSaveOnDoc) {
                                docContext.eventData = params.eventData;
                            }

                            ruleEngine.doPostSaveEvent(finalDocument, docContext, function () {
                                    logger.info("Post save done for update of " + finalDocument.documentId);
                                }
                            );

                        } else {
                            logger.info("Break the post save rule due to looping prevention.");
                        }

                        deferred.resolve(finalDocument);

                        // push the document to clients. This doesn't have to wait for the post save rules because if the post saves
                        // the document again, another push would take place on the save.
                        vDocuments.push(finalDocument);
                        documentPusher.pushDocumentVersions(vDocuments, finalDocument);
                    }, function errorFunc(err){
                        logger.error("Error updating document: " + err.message);
                        deferred.reject(new Error(err));
                    });

                }, function indexingError(error) {

                    logger.error("Error inserting document: " + error);
                    deferred.reject(error);

                });


            }

            return deferred.promise;
        }).catch(function (error) {
            if (!error.name) {
                error = new Error(error);
            }

            if (lock) {
                logger.info("Unlocking after updating failed document: " + documentId);
                DistributedLock.unlockDocument(lock);
            }

            return Promise.reject(error);
        });

}

function checkUpdateParams(params){

    //perform param check
    var paramError = null;

    if(params.newDocument == null){
        paramError = 'New document object required in update operation not found.';
    }else if(params.newDocument.documentId == null && params.documentId == null){
        paramError = 'Document Id must be provided in update.';
    }else if(params.userId == null){
        paramError = 'User Id must be provided in update.';
    }

    return paramError;

}

function update(documentId, newDocument, userId, options) {

    var updateParams = {
        documentId: documentId,
        newDocument: newDocument,
        updates: options.updates,
        userId: userId,
        isDeepDiff : options.isDeepDiff,
        templateId: options.templateId,
        connectionName : options.connectionName,
        isUpdateFromPreviousVersion: options.isUpdateFromPreviousVersion,
        fullDomainUrl: options.fullDomainUrl,
        eventData: options.eventData,
        makeCurrentVersion: options.makeCurrentVersion,
        importPreviousVersion: options.importPreviousVersion,
        documentSession: options.documentSession
    };

    return doUpdate(updateParams);

}

function jsonPatchUpdate(documentId, newDocument, userId, options){

    options.isDeepDiff = true;

    return update(documentId, newDocument, userId, options);
}

function checkIfDocumentExists(newDoc) {

  const systemType = newDoc.systemHeader ? newDoc.systemHeader.systemType : null;
  if (systemType === SharedConstants.SYSTEM_TYPE_ACCOUNT) {
    return getAccountService().checkExistedAccountOnUpdate(newDoc);
  } if (systemType === SharedConstants.SYSTEM_TYPE_ACCOUNT_SECURITY) {
    return getAccountService().checkExistedAccountSecurityOnUpdate(newDoc);
  } if (systemType === SharedConstants.SYSTEM_TYPE_ACCOUNT_CONTROL) {
    return getAccountService().checkExistedAccountControlOnUpdate(newDoc);
  } else {
    return false;
  }
}

module.exports = {

    update: update,

    jsonPatchUpdate: jsonPatchUpdate,

    deepDiffUpdate: jsonPatchUpdate
};
