'use strict';

var q = require("q");

var DataServiceUtil = require('./../utils/DataServiceUtil');
var DistributedLock = require('../../lock/DistributedLock');
var AccessManager = require('../../../shared/access/AccessManager');
var UtilDocumentId = require('../../../shared/utils/UtilDocumentId');
var templateValidator = require('../../validation/TemplateValidator');
var ruleEngine = require('../../ruleengine/RuleEngine');
var UtilError = require('./../../../shared/utils/UtilError');
var ElasticIndexingService= require('../../search/ElasticIndexingService');
var documentPusher = require('../../push/DocumentPusher');
var SharedConstants = require('../../../shared/constants/SharedConstants');
var logger = require('../../utils/Logger').loggers.get('db');
var findService = require('./FindService');
const DocumentIdValidator = require('../../../shared/utils/DocumentIdValidator');
var templateService = require('./TemplateService');
var ServerAccessManager = require('../utils/ServerAccessManager');
var serverUtilError = require('../../../server/utils/UtilError');
var UtilApplication = require('../../../shared/utils/UtilApplication');
var UtilHost = require('../../utils/UtilHost');
var dbConfig = require('../providers/DatabaseConfig');
const UtilDocument = require('../../../shared/utils/UtilDocument');
const PasswordHelper = require('../../account/PasswordHelper');

var accountService;

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

    return accountService;
};

function insert(newRecord, userId, options) {
    var deferred = q.defer();
    var connectionName;
    if (options){
        connectionName = options.connectionName;
    }
    var lock;
    logger.debug("Inserting new document...");

    if (!newRecord) {
        logger.debug("Document not provided in insert method");
        deferred.reject(new Error("A document must be provided in the insert method"));
    } else if (!newRecord.systemHeader) {
        logger.debug("systemHeader not provided in insert method");
        deferred.reject(new Error("A systemHeader must be provided in the insert method"));
    } else if (!userId) {
        logger.debug("User not provided in insert method");
        deferred.reject(new Error("A user must be provided in the insert method"));
    } else {
        DistributedLock.lockDocument(newRecord.documentId).then(function documentLocked(retrievedLock) {
            lock = retrievedLock;
            return checkIfDocumentExists(newRecord, connectionName);
        }).then(function (exists) {
            if (exists) {
                return Promise.reject(new Error("Document exists: " + newRecord.documentId));
            } else {

                // get the account, which needs to be read from the database
                // because the user passed in will be from Passport.js and may not have
                // the keys in it. Getting the user from the database also ensures the keys are up to date

                return DBProvider.accountDataService.getAccountById(userId);
            }
        }).then(function getAccountSuccess(dbUser) {
            if (!dbUser) {
                return Promise.reject(new Error("Couldn't get user " + userId));
            } else {
                logger.debug("Got user in insert: " + dbUser.documentId);

                if (newRecord.systemHeader && newRecord.systemHeader.templateId) {
                    var templateId = newRecord.systemHeader.templateId;

                    logger.debug("Inserting document of type: " + newRecord.systemHeader.systemType
                        + " - has template: " + templateId);

                    templateService.getTemplate(templateId, connectionName).then(
                        function successFunc(template) {
                            logger.debug("Got template in insert: " + template.documentId);
                            if (!template) {
                                deferred.reject('A valid template id that the user has access ' +
                                    'to must be specified in the insert');
                            } else if (!dbUser) {
                                deferred.reject('User not found when creating document');
                            } else {
                                doInsert(connectionName, dbUser, newRecord, template, lock, options).then(
                                    function successFunc(data) {
                                        deferred.resolve(data);

                                        logger.debug("Successfully inserted. Triggering post insert " +
                                            "actions for doc: " + data.documentId);

                                        var documentContext = {};
                                        documentContext.user = dbUser;
                                        documentContext.documentSession = options.documentSession;
                                        documentContext.isNew = true;
                                        ruleEngine.doPostSaveEvent(data, documentContext, function (result) {
                                            logger.info("Post save event completed for document: " + data.documentId);
                                        });

                                        documentPusher.pushDocumentVersions([data], data);
                                        logger.info("Pushed document: " + data.documentId);
                                    }, function errorFunc(err) {
                                        logger.warn("Insert document errors: " + err);
                                        deferred.reject(err);
                                    }
                                );
                            }
                        },
                        function errorFunc(err) {
                            logger.debug("No template found for id: " + templateId);
                            deferred.reject(err);
                        }
                    );
                } else {
                    logger.debug("Inserting document with no template specified of type: " + newRecord.systemHeader.systemType);

                    doInsert(connectionName, dbUser, newRecord, null, lock, options).then(
                        function successFunc(data) {
                            logger.info("Inserted document with no template id.");
                            deferred.resolve(data);
                        }, function errorFunc(err) {
                            logger.error("Error inserting document with no template id: " + JSON.stringify(err));
                            deferred.reject(err);
                        }
                    );
                }
            }
        }).catch(function(e){
            logger.error(e);
            deferred.reject(e);
        });
    }

    return deferred.promise;
}


const doInsert = async (connectionName, user, document, template, lock, options) => {

    const doSaveServer = () => {
        var deferred = q.defer();

        var docContext = {};
        docContext.user = user;
        docContext.documentSession = options.documentSession;
        docContext.isNew = true;

        ruleEngine.doPreSaveServer(document, docContext, function (ruleResult) {

            var status = ruleResult ? ruleResult.status : SharedConstants.STATUS_SUCCESS;
            if (!status || status.toLowerCase() !== SharedConstants.STATUS_ERROR) {

                // add the system header, which is needed by all records
                DataServiceUtil.writeSystemHeader(document,
                    SharedConstants.INSERT_OPERATION_TYPE, user, template);

                DataServiceUtil.imposeStandardDateTypeInSystemHeader(document);

                delete document[SharedConstants.MAKE_CURRENT_VERSION_KEY];

                UtilDocument.removeDisableSaveFieldValues(document, template);

                // create the document._id so it is used as the Elastic indexing _id
                var indexId = DBProvider.dataInsertService.getDocumentAndIndexId(document);
                var dsConfig = dbConfig.getDataServicesConfig(connectionName);
                                
                ElasticIndexingService.index(indexId, document, dsConfig).
                then(function indexingSuccess() {

                    processUploadDocumentToMakeCurrentVersion(document, connectionName,function(err,result) {
                      if (err) {
                        logger.error("Error processing upload document to make current version: " + JSON.stringify(err));
                        deferred.reject(err);
                      } else {

                        // insert array containing everything needed for the operation,
                        // so that either everything succeeds or everything fails
                        var documentsToInsert = [document];

                        // Insert all documents in a bulk operation, to ensure they all get written.
                        // If they were written separately then
                        // we couldn't guarantee that they would all be written if the process was interrupted,
                        // this is what makes it
                        // effectively transactional
                        DBProvider.dataInsertService.insert(documentsToInsert, connectionName).then(
                          function success(savedRecord) {
                            // Fetch the document

                            if (savedRecord && savedRecord.documentId) {
                              logger.info("Inserted document: " + savedRecord.documentId);

                              // the insert operation returns an array of documents,
                              // because we're only inserting one we can return the first doc
                              deferred.resolve(savedRecord);
                            } else {
                              var msg = "Invalid saved record returned by insert operation";
                              logger.error(msg);
                              deferred.reject(new Error(msg));
                            }

                            if (lock) {
                              logger.info("Unlocking document after insert: " + document.documentId);
                              DistributedLock.unlockDocument(lock);
                            }
                          }, function error(err) {
                            logger.error("Error inserting document: " + JSON.stringify(err));
                            if (lock) {
                              logger.info("Unlocking document after insert error: " + document.documentId);
                              DistributedLock.unlockDocument(lock);
                            }
                            deferred.reject(err);
                          });
                      }
                    });
                }, function indexingError(error) {

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

                });
            } else {

                var errorMsg = "Error in PreSaveSaver rule processing: " +
                ruleResult ? ruleResult.message : null;
                logger.error(errorMsg);
                deferred.reject(new Error(errorMsg));

            }
        });

        return deferred.promise;
    };

    const ensureDocumentId = () => {
        if (!document.documentId) {
            var documentId = UtilDocumentId.generateId();
            logger.debug("setting documentId: " + documentId);
            document.documentId = documentId;
        } else {
            var documentId = document.documentId;
            logger.debug("using passed in documentId: "+ documentId);
        }

        const validationResult = DocumentIdValidator.validateDocumentId(document.documentId, config.get('documentIdRules'));
        if (!validationResult.valid) {
            throw new Error(validationResult.reason);
        }
    };

    const dispatchActivateEmailIfAccount = async (fullDomainUrl, user) => {

        const isAccountType = document.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_ACCOUNT;
        if (!isAccountType || (template && template.disableAccountVerificationEmail)) {
          return;
        }

        const accountSecurity = await getAccountService().getAccountSecurityDocument(document.documentId);
        if (accountSecurity) {
            const account = { email: document.email, pass: accountSecurity.password, documentId: document.documentId };
            try {
                const activateDoc = await accountService.createAccountActivateDocument(account, user);
                await dispatchActivateAccountLink(account, fullDomainUrl, activateDoc);
            } catch (err) {
                logger.error('Error dispatching account activation email:');
                logger.error(err);
            }
        }

        // END. Helpers ---

        function dispatchActivateAccountLink(account, fullDomainUrl, activateDoc) {
            return new Promise((resolve, reject) => {
                const mailHelper = require('../../email/EmailHelper');
                mailHelper.dispatchActivateAccountLink(activateDoc, fullDomainUrl, (err, result) => {
                        if (err) {
                            reject(err);
                        } else {
                            resolve(result);
                        }
                    }
                );
            });
        }

    };

    const createSecurityIfAccount = async () => {
        if (document.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_ACCOUNT) {
            try {
                let accountSecurity = await getAccountService().getAccountSecurityDocument(document.documentId);
                if (!accountSecurity) {
                    let accounts = await getAccountService().createSecurityFromUserAccount(document,
                        document.email, null, user.documentId);

                    await DBProvider.dataInsertService.insert(accounts, connectionName);
                } else {
                    logger.info("Account security document for " + document.documentId + " already exist!");
                }

                if (document.password) {
                    document.password = "*";
                }

                return document;
            } catch (err) {
                var errorMsg = "Error creating control and security account: " + err.message;
                logger.error(errorMsg);
                throw new Error(errorMsg);
            }
        }
    };

    const preProcessFields = async () => {
        try {
            //require PreProcessor only when needed, dbProvider.counterService is null if placed at the top
            var PreProcessor = require('./../preprocessors/PreProcessor');
            return await PreProcessor.preProcessSaveFields(template, document, SharedConstants.INSERT_OPERATION_TYPE,
                null, null, user.documentId);
        } catch (err) {
            var errorMsg = "Error preprocessing save fields on insert: " + err;
            logger.error(errorMsg);
            throw new Error(errorMsg);
        }
    };

    logger.debug("Performing document insert...");

    // insert process starts here

    await hasPermission(user, template);
    await ServerAccessManager.writeKeysToDocument(document, template, user);

    UtilApplication.applyApplicationIds(document, template);

    ensureDocumentId();
    DataServiceUtil.applySystemType(document, template);
    DataServiceUtil.setDefaultValues(document, template);

    //validate template before saving
    await templateValidator.templateValidatorAndMapping(
        document, template, user.documentId, SharedConstants.INSERT_OPERATION_TYPE);

    await createSecurityIfAccount();
  
    await preProcessFields();

    await checkAccountControlDocumentExists(document);

    const saveResult = await doSaveServer();

    // do not use await so that we don't need to wait for the account activation email to dispatch
    // only dispatch email if account successfully saved
    dispatchActivateEmailIfAccount(options.fullDomainUrl, user);

    return saveResult;
};

async function hasPermission(user, template) {
    // the template needs to be passed in as the document for create because the document hasn't been created yet so won't
    // have had the keys from the template added. We can't add the attachKeys before doing this because the user would then
    // incorrectly have access because the user attach keys would be added to the document
    const allowed = await ServerAccessManager.checkAccess(template, user, SharedConstants.OPERATION_TYPE_CREATE);
    if(!allowed){
        throw UtilError.createForbiddenError(template, SharedConstants.OPERATION_TYPE_CREATE);
    }
    return true;
}

async function checkIfDocumentExists(newDoc, connectionName) {
    const options = {};
    if (newDoc && newDoc.systemHeader &&  newDoc.systemHeader.shouldMakeCurrentVersion){
      options.excludeDeleted = true;
    }

    const exists = await documentExists(newDoc.documentId, connectionName, options);
    if (exists) {
        return true;
    }

    const systemType = newDoc.systemHeader.systemType;
    if (systemType === SharedConstants.SYSTEM_TYPE_ACCOUNT) {
      return getAccountService().checkExistedAccount(newDoc);
    }

    const parentDocId = newDoc.accountRel && newDoc.accountRel[0] && newDoc.accountRel[0].documentId;
    if (!parentDocId) {
      return false;
    }

    let fetchFN;
    switch (systemType) {
        case SharedConstants.SYSTEM_TYPE_ACCOUNT_SECURITY:
            fetchFN = getAccountService().getAccountSecurityDocument;
            break;
        case SharedConstants.SYSTEM_TYPE_ACCOUNT_CONTROL:
            fetchFN = getAccountService().getAccountControlDocument;
            break;
        default:
            return false;
      }

    const result = await fetchFN(parentDocId);
    if (result) {
      const name = newDoc.accountRel[0].name;
      const errorMsg = systemType + " for account: " + name + " (" + parentDocId + ") already exists.";
      throw new serverUtilError.AccountUniqueResultError(errorMsg);
    }

    return false;
}

function documentExists(documentId, connectionName, options) {
    return DBProvider.dataInsertService.documentExists(documentId, connectionName, options);
}

function processUploadDocumentToMakeCurrentVersion(document,connectionName,callBack){


  if (document.systemHeader && document.systemHeader.shouldMakeCurrentVersion){
    DBProvider.dataInsertService.updateDeletedDocument(document.documentId, connectionName).then(
      function funcSuccess(deletedDocument) {
        if (deletedDocument && deletedDocument.systemHeader){
          document.systemHeader.previousVersionId = deletedDocument.systemHeader.versionId;
          delete document.systemHeader.shouldMakeCurrentVersion;
        }
        callBack(null, true);
      },
      function functionError(err){
        callBack(err);
      }
    );
  }
  else {
    callBack(null, true);
  }


}

async function checkAccountControlDocumentExists(document) {
  //if accountControl document created from ruleset check for duplication - mantis #16712
  if (document.systemHeader && document.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_ACCOUNT_CONTROL
    && document.accountRel && document.accountRel[0] && document.accountRel[0].documentId){
    const accountDocId = document.accountRel[0].documentId;
    const result = await getAccountService().getAccountControlDocument(accountDocId);
    if (result) {
      const name = document.accountRel[0].name ?  document.accountRel[0].name : '';
      const errorMsg =  "There is already an accountControl document for account: " +  name + "(" +  accountDocId + ")";
      logger.error(errorMsg);
      throw new Error(errorMsg);
    }
  }
}

module.exports = {
    insert: insert,

    documentExists: documentExists
};
