'use strict';

var q = require("q");
var AccountHelper = require('../../account/AccountHelper');
var TemplateService = require('./TemplateService');
var PasswordHelper = require('../../account/PasswordHelper');
var DataService = require('../DataService');
var _ = require('lodash/lang');
var DistributedLock = require('../../lock/DistributedLock');
var UtilDocumentId = require('../../../shared/utils/UtilDocumentId');
var UtilError = require('../../utils/UtilError');
var redisAccountService = require('../../redis/RedisAccountDataService');
var ErrorCodes = require("../../../shared/constants/ErrorCodes");
var counterService = require('../dataServices/CounterService');
const lodashCloneDeep = require('lodash.clonedeep');
let socket;

//Helper, private and isn't exposed. Uses the databaseService directly for looking up documents
function directFind(query){

    var deferred = q.defer();

    //Uses the database service for finding docs directly. The findService.find has limitations because APIs use it.
    DBProvider.dataFindService.find({
        query, "includeAccountSecurity" : true,  useCurrentVersion: true
    }, null, function(err, results){
        if (err) {
            deferred.reject(err);
        } else {

            var finalResult = [];

            //manually check if deleted because it's difficult design a query that works for it on mongo
            //mongo has {$ne: false} notation
            results.forEach(function (each) {
                var deleted = each.systemHeader.deleted;
                if (!deleted || deleted === false || deleted === 'false') {
                    finalResult.push(each);
                }
            });

            deferred.resolve(finalResult);
        }
    });

    return deferred.promise;
}

async function updateLoginDocument(user, loginSuccess) {
    const result = await directFind({"accountRel.documentId" : user.documentId, 
        "systemHeader.systemType": SharedConstants.SYSTEM_TYPE_LOGIN});

    let accountLogin;
    
    if (result && result.length) {
        accountLogin = result[0];
        logger.info("Login Document found: " + accountLogin.documentId);

        let updates = {};
        if (loginSuccess) { 
            await counterService.increase(accountLogin.documentId, "loginSuccessCount");
            updates.lastLoginSuccessDate = new Date();
        } else {
            await counterService.increase(accountLogin.documentId, "loginFailureCount");
            updates.lastLoginFailureDate = new Date();
        }

        const disableVersioning = true;
        await directUpdate(accountLogin.documentId, updates, user.documentId, disableVersioning);
    } else {
        logger.info("No login document found for user: " + user.documentId);

        var templateId = config.get("loginDocumentTemplateId");
        if (!templateId) { //login doc creation not properly configured, skipping
            return;
        }

        const loginDocTemplate = await TemplateService.getTemplate(templateId);
        if (!loginDocTemplate) {  //loginDocTemplate not found, skipping
            return;
        }

        const loginDoc = AccountHelper.buildLoginDocument(user, loginDocTemplate);
        if (loginSuccess) { 
            loginDoc.loginSuccessCount = 1;
            loginDoc.lastLoginSuccessDate = new Date();
        } else {
            loginDoc.loginFailureCount = 1;
            loginDoc.lastLoginFailureDate = new Date();
        }

        accountLogin = await DataService.insert(loginDoc, user.documentId);
        logger.info("Login document " + accountLogin.documentId + " created for user: " + user.documentId);
    }
}

function directUpdate(documentId, updates, userDocumentId, disableVersioning){

    var current;
    var updated;
    var documentEditLock;

    return Promise.resolve().then(function () {
        return directFind({"documentId" : documentId});
    }).then(function (docs) {
        if(!docs || docs.length === 0){
            throw new Error("Current document not found.");
        }
        current = docs[0];

        // 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(lock) {
        documentEditLock = lock;

        //extract the index id of the document in elastic. Read before calling clone because clone affects the _id property
        var elasticIndexId = DBProvider.dataUpdateService.getIndexId(current);

        updated = lodashCloneDeep(current);

        //APPLY UPDATES
        Object.keys(updates).forEach(function (key) {
            updated[key] = updates[key];
        });

        //APPLY SYSTEM HEADER CHANGES
        var now = new Date();
        //the current version will now become just another version
        var version = lodashCloneDeep(current);
        //remove the primary key so the version document will be inserted. this makes sure that we are always
        //updating the original document and inserting the version documents
        if (version._id) {
            delete version._id;
        }
        version.systemHeader.currentVersion = false;
        version.systemHeader.serverUpdatedDate = now;

        //the updated version will be come the current version
        updated.systemHeader.versionId = UtilDocumentId.generateId();
        updated.systemHeader.currentVersion = true;
        updated.systemHeader.previousVersionId = current.systemHeader.versionId;

        updated.systemHeader.createdBy = userDocumentId;

        updated.systemHeader.serverCreatedDate = now;
        updated.systemHeader.serverUpdatedDate = now;

        var updateParams = {
            documentId: documentId,
            finalDocumentIndexingId: elasticIndexId,
            oldDocument: current,
            updateDocument: updated,
            versionDocuments: disableVersioning ? [] : [version]
        };

        return DBProvider.dataUpdateService.update(updateParams);

    }).then(function () {

        unlock();

        // remove security and control documents, updated version will be fetched from db
        redisAccountService.removeAccountControl(userDocumentId);
        redisAccountService.removeAccountSecurity(userDocumentId);

        return updated;

    }).catch(function (err) {

        logger.error("Error while performing AccountService helper directUpdate:");
        logger.error(err);
        unlock();
        throw err;

    });

    //helper
    function unlock(){
        if(documentEditLock){
            DistributedLock.unlockDocument(documentEditLock);
        }
    }

}

//Helper. returns single accountControlDocument
function getAccountUtilityDocuments(accountId, type) {

    var query = DBProvider.accountDataService.getAccountUtilityDocumentsQuery(accountId, type);
    return directFind(query);

}

//Helper. Returns a single account security document. Account security document is hidden from the user.
function getAccountSecurityDocument(accountId) {

    function fetchFromRedis() {
        return redisAccountService.getAccountSecurity(accountId).then(redisAccountSecurity => {
            return redisAccountSecurity ? redisAccountSecurity : fetchFromDB();
        });
    }

    function fetchFromDB() {
        return getAccountUtilityDocuments(accountId, SharedConstants.SYSTEM_TYPE_ACCOUNT_SECURITY)

            .then(function(results){

                if (results.length === 0) {
                    logger.info("Account " + accountId + " does not have an accountSecurity document.");
                    return null;
                }

                if(results.length > 1){
                    logger.warn("Account " + accountId + " has multiple accountSecurity documents. Only one is expected.");
                }

                var accountSecurity = results[0];
                if (accountSecurity) {
                    redisAccountService.setAccountSecurity(accountId, accountSecurity);
                }

                return accountSecurity;

            });
    }

    return fetchFromRedis();
}

//Returns single account control document. Account control document is read-only to the user.
function getAccountControlDocument(accountDocId) {

    function fetchFromRedis() {
        return redisAccountService.getAccountControl(accountDocId).then(accountControl => {
            return accountControl ? accountControl : fetchFromDB();
        });
    }

    function fetchFromDB() {

        return getAccountUtilityDocuments(accountDocId, SharedConstants.SYSTEM_TYPE_ACCOUNT_CONTROL).then(
            function(results) {
                if (results.length === 0) {
                    logger.info("Account " + accountDocId + " does not have an accountControl document.");
                    return Promise.resolve();
                }

                if (results.length > 1) {
                    const accountControlDocs = results.map(doc => doc.documentId);
                    logger.error("Account " + accountDocId + " has multiple accountControl documents: " +
                        JSON.stringify(accountControlDocs) + ". Only one is expected.");
                    return Promise.reject(new UtilError.AccountUniqueResultError());
                }

                var accountControl = results[0];
                if (accountControl) {
                    redisAccountService.setAccountControl(accountDocId, accountControl);
                }

                return accountControl;
            }
        );
    }

    return fetchFromRedis();
}

//Helper.
//Takes in an array of account utility documents (accountSecurity or accountControl) and extracts the parent account ID.
function getParentAccountId(utilDocuments){

    var parentAccountId = null;

    utilDocuments.some(function(doc){

        var hasOwner = doc.accountRel  && doc.accountRel[0] && doc.accountRel[0].documentId;

        if(hasOwner){
            parentAccountId = doc.accountRel[0].documentId;
        }

        return hasOwner; //if true then will break Array.some()

    });

    return parentAccountId;

}


function getParentAccountDocument(childAccount) {
    var parentAccountId = getParentAccountId([childAccount]);
    return getAccountById(parentAccountId);
}

function getAccountById(userId) {
    var deferred = q.defer();

    redisAccountService.getAccount(userId).then(userFromRedis => {
        if (userFromRedis) {
            deferred.resolve(userFromRedis);
        } else {
            DBProvider.accountDataService.getAccountById(userId).then(
                function successFunc(user) {
                    redisAccountService.setAccount(user);
                    deferred.resolve(user);
                },
                function ErrorFunc(err) {
                    deferred.reject(err);
                }
            );
        }
    });

    return deferred.promise;
};

/**
 * Note that password is now an object {value: string, hashed: bool}. Otherwise if password is a string it's expected to
 * be hashed (hased : true).
 * @param email
 * @param accountShouldBeVerified - whether the account should be verified
 * @param password
 * @param provider
 * @param accountSocialMediaId
 * @returns {Promise<void>}
 */
async function getAccount(email, accountShouldBeVerified, password, provider, accountSocialMediaId) {

    //start
    password = typeof password === "string" ? {value : password , hashed: true} : password;

    logger.debug("Getting Account: " + email);

    try {
        let account = await DBProvider.accountDataService.getAccountByQuery(email, accountSocialMediaId);
        if (!account) {
            throw new Error("Account not found.");
        }

        let accountControl = await getAccountControlDocument(account.documentId);
        let accountSecurity = await getAccountSecurityDocument(account.documentId);
      
        if (!accountControl) {
            throw new Error('Account has no control document and is not verified. Please see an administrator');
        }

        if (accountShouldBeVerified && !isAccountVerified(accountControl)) {
            throw new Error("Account not verified.");
        }

        if (provider && !securityCheck(accountSecurity, {"provider" : provider})) {
            throw new Error("No account with email/provider: " + email + "/" + provider);
        }

        if (password && !await isPasswordValid(accountSecurity, password)) {
            throw new Error("Invalid password.");
        }

        return Promise.resolve(account);
    } catch (err) {
        if (!err) {
            err = new Error('There was a problem logging you in. Please try again later or contact an administrator.');
        }

        return Promise.reject(err);
    }

    //helper. We store the verified status in account control documents.
    function isAccountVerified(accountControl) {
        return accountControl.verified === true;
    }

    //checks if the following params in the checks object is present in the account's account security document.
    function securityCheck(accountSecurity, checks) {
        if (checks && accountSecurity) {
            return Object.keys(checks).every(function(key){
                return checks[key] === accountSecurity[key];
            });
        }

        return accountSecurity != null;
    }

    //helper. Note that passwords are stored in account security documents
    async function isPasswordValid(security, passwordObject) {
        try {
            return await passwordMatches(passwordObject, security.password);
        } catch (err) {
            logger.error(err);
            return false;
        }

        //helper
        function passwordMatches(passwordObject, hashedPassword){

            return new Promise(function(resolve, reject){

                if(passwordObject.hashed){
                    return resolve(passwordObject.value === hashedPassword);
                }

                PasswordHelper.validatePassword(passwordObject.value, hashedPassword, function(err, matches){
                    if(err){
                        return reject(err);
                    }
                    resolve(matches);
                });
            });

        }
    }
};


function createRelatedAccounts(account, email, encryptedPass, createdById, accountInfo) {
    var accountSecurity, accountControl;
    var error;

    accountInfo = accountInfo ? accountInfo : account;

    return Promise.resolve()

        .then(function() {

            return getAccountSecurityDocument(account.documentId);

        })
        .then(function(accountSecurityDocument) {

            if (accountSecurityDocument) {

                accountSecurity = accountSecurityDocument;

            }

            var accSecurityTemplateId = config.get("accountSecurityTemplateId");
            return TemplateService.getTemplate(accSecurityTemplateId);

        }).then(function(accSecurityTemplate) {
            if (!accSecurityTemplate) {
                throw new UtilError.NoAccountSecurityTemplateError();
            }

            if (!accountSecurity) {
                accountSecurity = AccountHelper.buildAccountSecurityDocument(account,
                    encryptedPass,
                    accountInfo,
                    createdById,
                    accSecurityTemplate);
            }
            AccountHelper.inheritKeys(accSecurityTemplate, accountSecurity);

            return Promise.resolve([accountSecurity]);

        }).catch(function(e) {
            return Promise.reject(error ? error : e);
        });
}

function createSecurityFromUserAccount(account, email, encryptedPass, createdById) {

    if (encryptedPass) {

        return createRelatedAccounts(account, email, encryptedPass, createdById);

    } else {

        var deferred = q.defer();

        let pass = account.password;
        if (typeof pass === "object") {
            if (!pass.newPass) {
                deferred.reject(new Error("Password is not defined."));
                return deferred.promise;
            }

            pass = pass.newPass;
        }

        PasswordHelper.createHash(pass, function (hash) {
            createRelatedAccounts(account, email, hash, createdById).then(
                function(results) {
                    deferred.resolve(results);
                },
                function errorFunc(e) {
                    deferred.reject(e);
                });
        });

        return deferred.promise;
    }
}

function removeAccountPasswordPlaceHolder(securityAccount, userId) {
    return getParentAccountDocument(securityAccount).then(
        function(parentAccount) {
            if (parentAccount.password) {
                //delete password place holder
                return directUpdate(parentAccount.documentId, {password: ""}, userId);
            }
        },
        function(err) {
            return Promise.reject(err);
        }
    );
}

function checkExistedAccount(account) {

    var checkError;

    let aQuery = DBProvider.accountDataService.getCheckExistedAccountQuery(account.email);
    aQuery["systemHeader.systemType"] = SharedConstants.SYSTEM_TYPE_ACCOUNT;

    return Promise.resolve().then(function(){

        return directFind(aQuery);

    }).then(function(results) {

        if (results && results.length) {
            let message = "There is more than one account with the same email.";
            let error = UtilError.buildError("DuplicatedAccountEmailError",
                message,
                ErrorCodes.ERROR_DUPLICATE_ACCOUNT_EMAIL);

            checkError = error;

            return Promise.reject();
        }

        if (account.apiKey && account.apiHosts) {
            return getAccountByApiKey(account.apiKey, account.apiHosts);
        }
        if (account.accountPublicURL) {
            return getAccountByPublicURL(account.accountPublicURL);
        }
        return Promise.resolve(false);

    }).then(function(foundAccount) {

        if (foundAccount) {
            let error;
            if (account.apiKey && account.apiHosts) {
                let message = "There is more than one account with the same API key.";
                error = UtilError.buildError("DuplicatedAccountAPIError",
                    message,
                    ErrorCodes.ERROR_DUPLICATE_ACCOUNT_API_KEY);
            } else if (account.accountPublicURL) {
                let message = "There is more than one account with the same public URL: " + account.accountPublicURL;
                error = UtilError.buildError("DuplicatedAccountPublicURLError",
                    message,
                    ErrorCodes.ERROR_DUPLICATE_ACCOUNT_PUBLIC_URL);
            }

            checkError = error;

            return Promise.reject();
        }

        if (!account.accountPublicURL) return Promise.resolve(false);

        return getAccountByPublicURL(account.accountPublicURL);

    }, function(error) {

        if (error && (error.errorCode === ErrorCodes.ERROR_NOT_FOUND_ACCOUNT_API_KEY)) {

            if (!account.accountPublicURL) return Promise.resolve(false);

            return getAccountByPublicURL(account.accountPublicURL);
        }

        if (error && (error.errorCode === ErrorCodes.ERROR_NOT_FOUND_ACCOUNT_PUBLIC_URL)) {

            return Promise.resolve(false);
        }

        checkError = checkError ? checkError : error;

        return Promise.reject();

    }).then(function(foundAccount) {

        if (foundAccount) {
            let message = "There is more than one account with the same public URL: " + account.accountPublicURL;
            let error = UtilError.buildError("DuplicatedAccountPublicURLError",
                message,
                ErrorCodes.ERROR_DUPLICATE_ACCOUNT_PUBLIC_URL);

            checkError = checkError ? checkError : error;
            return Promise.reject();
        }

        return Promise.resolve(false);

    }, function(error) {

        if (error && (error.errorCode === ErrorCodes.ERROR_NOT_FOUND_ACCOUNT_PUBLIC_URL)) {

            return Promise.resolve(false);
        }

        checkError = checkError ? checkError : error;
        return Promise.reject();

    }).catch(function(e) {
        return Promise.reject(checkError ? checkError : e);
    });
}

function checkExistedAccountOnUpdate(account) {

  var checkError;

  let aQuery = DBProvider.accountDataService.getCheckExistedAccountQuery(account.email);
  aQuery["systemHeader.systemType"] = SharedConstants.SYSTEM_TYPE_ACCOUNT;

  return Promise.resolve().then(function(){

    return directFind(aQuery);

  }).then(function(results) {

    if (results &&  results.length && ( results.length > 1 || results[0].documentId !== account.documentId) ) {
      let message = "There is more than one account with the same email.";
      let error = UtilError.buildError("DuplicatedAccountEmailError",
        message,
        ErrorCodes.ERROR_DUPLICATE_ACCOUNT_EMAIL);

      checkError = error;

      return Promise.reject(checkError);
    }

    return Promise.resolve(false);

  }).catch(function(e) {
    return Promise.reject(checkError ? checkError : e);
  });
}

const checkExistedAccountSecurityOnUpdate = async (accountSecurity) => {
    const { apiKey } = accountSecurity;
    if (apiKey) {
        const query = {
            apiKey,
            "systemHeader.systemType" : SharedConstants.SYSTEM_TYPE_ACCOUNT_SECURITY
        };

        const results = await directFind(query);
        if (results.length > 1 || (results.length && results[0].documentId !== accountSecurity.documentId)) {
            let error = UtilError.buildError("DuplicatedAccountAPIError",
                "There is more than one account with apiKey/apiHosts: " + apiKey + "/" + apiHosts,
                ErrorCodes.ERROR_DUPLICATE_ACCOUNT_API_KEY);
            throw error;
        }
    }

    return false;
};

const checkExistedAccountControlOnUpdate = async (accountControl) => {
    const { accountPublicURL } = accountControl;
    if (accountPublicURL) {
        const query = {
            accountPublicURL,
            "systemHeader.systemType" : SharedConstants.SYSTEM_TYPE_ACCOUNT_CONTROL
        };

        const results = await directFind(query);
        if (results.length > 1 || (results.length && results[0].documentId !== accountControl.documentId)) {
            let message = "There is more than one account with the accountPublicURL: " + publicURL;
            let error = UtilError.buildError("DuplicatedAccountPublicURLError",
                message, ErrorCodes.ERROR_DUPLICATE_ACCOUNT_PUBLIC_URL);
            throw error;
        }
    }


    return false;
};

function getAccountByApiKey(apiKey, apiHosts) {


    return DBProvider.accountDataService.getAccountByApiKey(apiKey, apiHosts).then(function(results){

        if(!results || results.length < 1){

            let error = UtilError.buildError("NoAccountAPIError",
                "No account with apiKey/apiHosts: " + apiKey + " / " + apiHosts,
                ErrorCodes.ERROR_NOT_FOUND_ACCOUNT_API_KEY);
            logger.error(error);
            throw error;
        }

        if (results.length > 1) {

            let error = UtilError.buildError("DuplicatedAccountAPIError",
                "There is more than one account with apiKey/apiHosts: " + apiKey + "/" + apiHosts,
                ErrorCodes.ERROR_DUPLICATE_ACCOUNT_API_KEY);
            logger.error(error);
            throw error;
        }

        var accountId = getParentAccountId(results);

        if(!accountId){
            let error = UtilError.buildError("OrphanedAccountAPIError",
                "Taken apiKey/apiHosts but there has no account: " + apiKey + "/" + apiHosts,
                ErrorCodes.ERROR_ORPHANED_ACCOUNT_API_KEY);
            logger.error(error);
            throw error;
        }

        return getAccountById(accountId);

    });
}

function getAccountByPublicURL(publicURL) {

    if(!publicURL){
        return Promise.reject(new Error("accountPublicURL parameter undefined"));
    }

    logger.debug("Getting Account via name: " + publicURL);

    var query = {
        "accountPublicURL" : publicURL,
        "publicAccount" : true,
        "systemHeader.systemType" : SharedConstants.SYSTEM_TYPE_ACCOUNT_CONTROL
    };

    return directFind(query).then(function(results){

        if(!results || results.length < 1){
            let error = UtilError.buildError("NoAccountPublicURLError",
                "No account with public URL: " + publicURL,
                ErrorCodes.ERROR_NOT_FOUND_ACCOUNT_PUBLIC_URL);
            logger.error(error);
            throw error;
        }

        var accountId = getParentAccountId(results);

        if(!accountId){
            let error = UtilError.buildError("NoAccountPublicURLError",
                "Taken publicURL but there has no account: " + publicURL,
                ErrorCodes.ERROR_NOT_FOUND_ACCOUNT_PUBLIC_URL);
            logger.error(error);

            throw error;
        }

        // check if results contains multiple accounts
        let sameAccount = results.every(function (doc) {
            var hasOwner = doc.accountRel && doc.accountRel[0] && doc.accountRel[0].documentId;
            return hasOwner && accountId === doc.accountRel[0].documentId;
        });

        if (!sameAccount) {
            let message = "There is more than one account with the accountPublicURL: " + publicURL;
            let error = UtilError.buildError("DuplicatedAccountPublicURLError",
                message,
                ErrorCodes.ERROR_DUPLICATE_ACCOUNT_PUBLIC_URL);
            logger.error(error);

            throw error;
        }


        return getAccountById(accountId);

    });

}

const updateAccountSecurityPassword = async (userId, fieldName, hash) => {
    var accountSecurity = await getAccountSecurityDocument(userId);
    if (!accountSecurity) {
        var errMsg = "Account security document does not exist for account : " + userId;
        throw new Error(errMsg);
    }

    accountSecurity[fieldName] = hash;
    var updates = {};
    updates[fieldName] = hash;

    return directUpdate(accountSecurity.documentId, updates, userId);
};
function getAccountGroupConfigDocument(accountDocId) {

  function fetchFromRedis() {
    return redisAccountService.getAccountGroupConfig(accountDocId).then(accountGroupConfig => {
      return accountGroupConfig ? accountGroupConfig : fetchFromDB();
    });
  }

  async function fetchFromDB() {

    //get accountControl document to find accountGroupConfig based on accountGroupConfigRel.documentId
    try {
      const accountControlDoc = await getAccountControlDocument(accountDocId);
      if (!accountControlDoc || (accountControlDoc && !accountControlDoc.accountGroupConfigRel)) {
        return Promise.resolve();
      }

      const accountGroupConfigId = accountControlDoc.accountGroupConfigRel[0].documentId;

      const query = {"documentId" : accountGroupConfigId};

      return directFind(query).then(function(results){
          if (results.length === 0) {
            logger.info("Account " + accountDocId + " does not have an accountGroupConfig document.");
            return Promise.resolve();
          }

          if (results.length > 1) {
            const accountGroupConfigs = results.map(doc => doc.documentId);
            logger.error("Account " + accountDocId + " has multiple accountGroupConfig documents: " +
                JSON.stringify(accountGroupConfigs) + ". Only one is expected.");
            return Promise.reject(new UtilError.AccountUniqueResultError());
          }

          const accountGroupConfig = results[0];
          if (accountGroupConfig) {
            redisAccountService.setAccountGroupConfig(accountDocId, accountGroupConfig);
          }

          return accountGroupConfig;
      }, function(err){
          return Promise.reject(err);
      });

    } catch (err){
      return Promise.reject(err);
    }
  }

  return fetchFromRedis();

}

const isValidUserWithAdditionalVerification = async(requestObject, account) => {
    const errMsg = "account additional verification failed";

    const fields = config.get("accountValidationFields");
    if (fields && fields.length > 0 && _.isObject(requestObject)) {
      let accountControl = await getAccountControlDocument(account.documentId);
      let accountSecurity = await getAccountSecurityDocument(account.documentId);

      for (let i=0; i<fields.length; i++){
        const item = fields[i];
        let flag = accountSecurity[item] && accountSecurity[item] === requestObject[item];
        if (!flag){
          flag = accountControl[item] && accountControl[item] === requestObject[item];
        }
        if (!flag){
          flag = account[item] && account[item] === requestObject[item];
        }
        if (!flag){
          return Promise.reject( new Error(errMsg));
        }
      }
      return Promise.resolve(true);
    } else {
      return Promise.resolve(true);
    }

};

module.exports = {

    checkExistedAccount: checkExistedAccount,

    checkExistedAccountOnUpdate: checkExistedAccountOnUpdate,

    checkExistedAccountSecurityOnUpdate,

    checkExistedAccountControlOnUpdate,

    removeAccountPasswordPlaceHolder: removeAccountPasswordPlaceHolder,

    getParentAccountDocument: getParentAccountDocument,

    createSecurityFromUserAccount: createSecurityFromUserAccount,

    getAccountById: getAccountById,

    getAccount: getAccount,

    getAccountByPublicURL: getAccountByPublicURL,

    getAccountByApiKey: getAccountByApiKey,

    updateLoginDocument,

    createPasswordResetDocument: function createPasswordResetDocument(requestObject){
        var deferred = q.defer();
        const email = _.isObject(requestObject) ? requestObject.email : requestObject;
        getAccount(email).then(function successFunc(user) {
            if (user) {
                isValidUserWithAdditionalVerification(requestObject, user).then(function successFunc(isValid) {
                    PasswordHelper.createResetTokenHash(function (hash) {
                        var passwordRestDoc = AccountHelper.createPasswordResetDocument(email, hash, user);
                        if (!passwordRestDoc.systemHeader.templateId){
                            var msg = "Please configure the password reset template with appropriate keys for locking " +
                                "down the password reset template";
                            deferred.reject(new Error(msg));
                            return;
                        }

                        const insertDoc = () => {
                            DataService.insert(passwordRestDoc, user.documentId).then(
                                function success(savedRecord) {

                                    logger.debug("Created password reset document: " + savedRecord.documentId);

                                    var doc = {};
                                    doc.email = user.email;
                                    doc.pass = hash;
                                    deferred.resolve(doc);

                                }, function error(err) {
                                    deferred.reject(err);
                                });
                        };

                        getAccountControlDocument(user.documentId).then(controlDoc => {
                            if (controlDoc.resetPasswordOnLogin && !controlDoc.resetPassHash) {
                                directUpdate(controlDoc.documentId,
                                    { resetPassHash: hash },
                                    user.documentId).then(() => {
                                    insertDoc();
                                }).catch(error => {
                                    deferred.reject(error);
                                });
                            } else {
                                insertDoc();
                            }
                        }).catch(error => {
                            deferred.reject(error);
                        });
                    });
                }, function errorFunc(err) {
                    logger.error(err);
                    deferred.reject({message: "No account found."});
                });
            } else {
                deferred.reject({message: "No account found."});
            }
        }, function errorFunc(err) {
            deferred.reject(err);
        });

        return deferred.promise;
    },

    resetPassword : function resetPassword(email, hash, newPassRaw){

        var dbUser;
        var hashedPass;

        return getUserFromPasswordResetRequest(email, hash).then(function successFunc(userAccount) {

            if(!userAccount){
                throw new Error("Invalid password reset request.");
            }

            dbUser = userAccount;

            return createHash(newPassRaw);

        }).then(function (hash) {

            hashedPass = hash;

            return getAccountSecurityDocument(dbUser.documentId);

        }).then(function(passwordDocument){
            return directUpdate(passwordDocument.documentId, {password : hashedPass}, dbUser.documentId);

        }).then(async function(result){
            // if the account is not activated and the activateAccountOnPasswordReset is true on server config and accountBlocked
            // is false on accountControl document, then activate the account and reset the password. mantis #13887
            const authConfig = config.get("authentication");
            let controlDoc = await getAccountControlDocument(dbUser.documentId);
            let updates = {
                resetPasswordOnLogin: false,
                resetPassHash: null
            };

            if (controlDoc && !controlDoc.verified && !controlDoc.accountBlocked
              && authConfig && authConfig.activateAccountOnPasswordReset ){
              // remove accountControl from redis, it will fetched from db with the updated version
              await redisAccountService.removeAccountControl(dbUser.documentId);

              //update the control document with verified is true.
              updates.verified = true;
              logger.debug("Activated the account on password reset : " + dbUser.documentId);
            }

            await directUpdate(controlDoc.documentId, updates, dbUser.documentId);


            // remove accountSecurity from redis, it will fetched from db with the updated version
            redisAccountService.removeAccountSecurity(dbUser.documentId);

           return new Promise(function (resolve, reject){
            directUpdate(dbUser.passwordResetDocumentId, {isActiveBool : false}, dbUser.documentId).then(function(data){
              if (!socket) {
                socket = require('../../socket/SocketServer');
              }

              socket.sendPasswordResetNotification(dbUser.documentId);
              resolve(data);
            }).catch(function(err){
              reject(err);
            });
          });

        });

        //helper
        function getUserFromPasswordResetRequest(email, hash){

            var resetDocument;

            return Promise.resolve().then(function () {

                return DBProvider.accountDataService.getPasswordResetDocument(hash);

            }).then(function (resetDoc) {
                resetDocument = resetDoc;

                if(resetDoc){
                    if (!resetDoc.passwordResetAccountDocumentId) {
                        throw new Error("passwordResetAccountDocumentId field not found in password reset template. "
                            + " Please rename accountId to passwordResetAccountDocumentId.");
                    }

                    return getAccountById(resetDoc.passwordResetAccountDocumentId);
                }
                return null;

            }).then(function (user) {

                if(!user){
                    return null;
                }

                if (user.email !== email) {
                    throw new Error("Invalid email id.");
                }

                user.passwordResetDocumentId = resetDocument.documentId;

                return user;

            });

        };

        //helper
        function createHash(newPass){
            return new Promise(function (resolve, reject){
                PasswordHelper.createHash(newPass, function (hash) {
                    resolve(hash);
                });
            });
        }
    },

    updatePassword : async (userId, fieldName, hash) => {
        let account = await getAccountById(userId);

        if (!account) {
            var msg = "No account found for id: " + userId;
            logger.error(msg);
            throw UtilError.AccountNotFoundError;
        }

        return updateAccountSecurityPassword(userId, fieldName, hash);
    },

    updateAccountSecurityPassword: updateAccountSecurityPassword,

    activateAccount : function (token) {

        var dbUser;

        return Promise.resolve().then(function () {

            return getUserFromAccountActivateRequest(token);

        }).then(function successFunc(account) {

            if(!account){
                throw new Error("Account to activate not found: ");
            }

            dbUser = account;

            return getAccountControlDocument(dbUser.documentId);

        }).then(function(control){

            // remove accountControl from redis, it will fetched from db with the updated version
            redisAccountService.removeAccountControl(dbUser.documentId);

            //returns the final and updated document
            return directUpdate(control.documentId, {verified : true}, dbUser.documentId);

        }).then(function (updatedControl) {

            return dbUser; //activate account expects the user account object to get returned

        }).catch(function (err) {
            logger.error(err);
        });

        function getUserFromAccountActivateRequest(hash){

          var resetDocument;

          return Promise.resolve().then(function () {

            return DBProvider.accountDataService.getPasswordResetDocument(hash);

          }).then(function (resetDoc) {
            resetDocument = resetDoc;

            if(resetDoc){
              if (!resetDoc.passwordResetAccountDocumentId) {
                throw new Error("passwordResetAccountDocumentId field not found in account activate document. "
                  + " Please rename accountId to passwordResetAccountDocumentId.");
              }

              return getAccountById(resetDoc.passwordResetAccountDocumentId);
            }
            return null;

          }).then(function (user) {

            if(!user){
              return null;
            }

            user.accountActivateDocumentId = resetDocument.documentId;

            return user;

          });

      }
    },

    deActivateAccount: async (documentId, userId, fullDomainUrl)  => {
        let account = await getAccountById(documentId);

        if (!account) {
            throw new Error("Account to deactivate not found: " + documentId);
        }

        let control = await getAccountControlDocument(documentId);
        let security = await getAccountSecurityDocument(documentId);

        control = await directUpdate(control.documentId, { verified: false }, userId);

        var mailHelper = require('../../email/EmailHelper');

        return await new Promise((resolve, reject) => {
            mailHelper.dispatchActivateAccountLink(
                { email: account.email, pass: security.password }, fullDomainUrl,
                (err, result) => {
                    if (err) {
                        reject(err);
                    } else {
                        redisAccountService.removeAccountControl(documentId);
                        resolve(control);
                    }
                }
            );
        });
    },

    getAccountControlDocument: getAccountControlDocument,

    getAccountSecurityDocument: getAccountSecurityDocument,

    getAccountGroupConfigDocument: getAccountGroupConfigDocument,

    //<editor-fold desc="Section exposed for tests. Do not use in APIs">

    _testUtils : {
        //exposed for tests. not intended for use in API
        getAccountSecurityDocument : getAccountSecurityDocument,

        //exposed for tests. not intended for use in API
        getAccountControlDocument : getAccountControlDocument,

        //exposed for tests. not intended for use in API
        //does not detect duplicates in access keys
        getAccountAccessKeys : function(accountId){
            return Promise.resolve().then(function(){
                return getAccountControlDocument(accountId);
            }).then(function(controlDoc){

                var result = [];
                if(controlDoc.accessKeys){
                    controlDoc.accessKeys.forEach(function(k){
                        result.push(k);
                    });
                }

                return result;

            });
        },

        //exposed for tests. not intended for use in API
        directUpdate: function(documentId, updates, userDocumentId) {
            logger.warn("******** directUpdate function exposed for tests. Not intented for use in API. ********");
            return directUpdate(documentId, updates, userDocumentId);
        },

        //throws an error if keyId is not full access to any user that holds it
        keyIdFullAccess : function(keyId){

            var query = {
                "systemHeader.systemType" : SharedConstants.SYSTEM_TYPE_ACCOUNT_CONTROL,
                "accessKeys.keyId" : keyId
            };

            return Promise.resolve().then(function(){
                return directFind(query);
            }).then(function(controls){
                controls.forEach(function(c){
                    c.accessKeys.forEach(function(k){
                        if(!isFullAccessRights(k.rights)){
                            throw new Error("Key is expected to be a full access key but it isn't: " + JSON.stringify(k));
                        }
                    });
                });
            });

            //helper
            function isFullAccessRights(rights){

                var readFound = false;
                var createFound = false;
                var updateFound = false;
                var deleteFound = false;

                rights.forEach(function(r){
                    switch (r){
                        case "Create" :
                            createFound = true;
                            break;
                        case "Read" :
                            readFound = true;
                            break;
                        case "Update" :
                            updateFound = true;
                            break;
                        case  "Delete" :
                            deleteFound = true;
                            break;
                    }
                });

                return readFound && createFound && updateFound && deleteFound;
            }
        },

        ACCOUNT_SECURITY_DETAILS: AccountHelper._testUtils.ACCOUNT_SECURITY_DETAILS,

        ACCOUNT_CONTROL_DETAILS : AccountHelper._testUtils.ACCOUNT_CONTROL_DETAILS


      },

    createAccountActivateDocument: function createAccountActivateDocument(account, user){
      var deferred = q.defer();

      PasswordHelper.createResetTokenHash(function (hash) {
        var passwordRestDoc = AccountHelper.createActivateAccountDocument(account, hash, user);
        if (!passwordRestDoc.systemHeader.templateId){
          var msg = "Please configure the password reset template with appropriate keys for locking " +
            "down the password reset template";
          deferred.reject(new Error(msg));
          return;
        }
        DataService.insert(passwordRestDoc, user.documentId).then(
          function success(savedRecord) {

            logger.debug("Created password reset document: " + savedRecord.documentId);

            var doc = {};
            doc.email = account.email;
            doc.token = hash;
            deferred.resolve(doc);

          }, function error(err) {
            deferred.reject(err);
          });

      });


      return deferred.promise;
    }


  //</editor-fold>

};
