/**
 * This is the data service that brings other data services together. There can be requests to different data services for different
 * operations. There may be more than one data service in a running instance of the application - possibly with different databases
 * in each data service instance. Eg. if AVL data is inserted into a another database but the rest of the data is inserted into
 * MongoDB
 */
'use strict';

var q = require("q");
var dbConfig = require('./providers/DatabaseConfig');

var insertService = require('./dataServices/InsertService');
var updateService = require('./dataServices/UpdateService');
var templateService = require('./dataServices/TemplateService');
var deleteService = require('./dataServices/DeleteService');
var findService = require('./dataServices/FindService');
var utilBanner = require('../utils/UtilBanner');

// named list of data sources. These will be initialised as each request is made
var dataSources = {
};

function getDataSource(options) {

    var deferred = q.defer();

    var isSecondaryConnection = false;
    var currentConnectionName = global.currentConnectionName;
    options = initOptions(options);
    if (!options.connectionName ){
        options.connectionName = global.currentConnectionName ?
            global.currentConnectionName : SharedConstants.DEFAULT_DB_CONNECTION;
    }
    else{
        isSecondaryConnection =true;
    }

    var connName = options.connectionName;
    var dsConfig = dbConfig.getDataServicesConfig(connName);
    if (dsConfig && dsConfig.providerName){
        if (!dataSources[connName]) {
            var dataSource = require("./providers/DatabaseProvider")(connName);
            if (isSecondaryConnection){
                global.currentConnectionName = currentConnectionName;
            }
            DBProvider.connection.initialise(connName, dbClose).then(function () {
                dataSources[connName] = dataSource.dataService;
                dataSources[connName].connectionName = connName;
                deferred.resolve(dataSources[connName]);
            },function errorFunc(err) {
                deferred.reject(err);
            });
        }
        else{
            dataSources[connName].connectionName = connName;
            deferred.resolve(dataSources[connName]);
        }
    }
    else{
        deferred.reject(new Error("Named connection does not exist: " + connName));
    }


    return deferred.promise;
}

function dbClose() {
    utilBanner.printCompletionBanner("Application shutdown completed.");
    process.exit();
}

async function initialise(options) {
    const systemTypeComponent = 'systemTypeComponent';
    if (config.get("dataServices")[systemTypeComponent]) {
        await getDataSource({ connectionName: systemTypeComponent });
    }

    return getDataSource(options);
}

function close(options) {
    const deferred = q.defer();

    options = initOptions(options);
    if (!options.connectionName ){
        options.connectionName = global.currentConnectionName ?
          global.currentConnectionName : SharedConstants.DEFAULT_DB_CONNECTION;
    }
  
    const connName = options.connectionName;
    DBProvider.connection.close(connName).then (
      function success() {  
        deferred.resolve("closed");
        dbClose();
      },
      function error(err) {
        logger.error(err);
        deferred.resolve("closed");
        dbClose();
     }
    );

    return deferred.promise;
}

function findOne(id, userId, options) {
    var deferred = q.defer();
    options = initOptions(options);
    getDataSource(options).then(function (dataSource) {
        deferred.resolve(findService.findOne(id, userId, options));
    },function errorFunc(err) {
        deferred.reject(err);
    });

    return deferred.promise;
}

function findTemplate(id, userId, options) {
    var deferred = q.defer();
    options = initOptions(options);
    getDataSource(options).then(function (dataSource) {
        deferred.resolve(findService.findTemplate(id, userId, options));
    },function errorFunc(err) {
        deferred.reject(err);
    });

    return deferred.promise;
}

function findAll(query, userId, sortQuery, allVersions, options) {
    var deferred = q.defer();
    options = initOptions(options);
    getDataSource(options).then(function (dataSource) {
        deferred.resolve(findService.findAll(query, userId, sortQuery, allVersions, options));
    },function errorFunc(err) {
        deferred.reject(err);
    });

    return deferred.promise;

}

function findVersions(ids, userId, sortQuery, allVersions, options) {
    var deferred = q.defer();
    options = initOptions(options);
    getDataSource(options).then(function (dataSource) {
        deferred.resolve(findService.findVersions(ids, userId, sortQuery, allVersions, options));
    },function errorFunc(err) {
        deferred.reject(err);
    });

    return deferred.promise;

}


function insert(newRecord, userId, options) {

    var deferred = q.defer();
    options = initOptions(options);
    getDataSource(options).then(function (dataSource) {
        deferred.resolve(insertService.insert(newRecord, userId, options));
    },function errorFunc(err) {
        deferred.reject(err);
    });

    return deferred.promise;
}

function update(id, updates, userId, options) {
    var deferred = q.defer();
    options = initOptions(options);
    getDataSource(options).then(function (dataSource) {
        deferred.resolve(updateService.update(id, updates, userId, options));
    },function errorFunc(err) {
        deferred.reject(err);
    });

    return deferred.promise;
}

function jsonPatchUpdate(id, updates, userId, options) {

    var deferred = q.defer();
    options = initOptions(options);
    getDataSource(options).then(function (dataSource) {
        deferred.resolve(updateService.jsonPatchUpdate(id, updates, userId, options));
    }, function errorFunc(err) {
        deferred.reject(err);
    });

    return deferred.promise;
}

function destroy(id, userId, options) {

    var deferred = q.defer();
    options = initOptions(options);
    getDataSource(options).then(function (dataSource) {
        deferred.resolve(deleteService.destroy(id, userId, options));
    },function errorFunc(err) {
        deferred.reject(err);
    });

    return deferred.promise;

}

function getTemplate(templateId, options) {

    var deferred = q.defer();
    options = initOptions(options);
    getDataSource(options).then(function (dataSource) {
        deferred.resolve(templateService.getTemplate(templateId, dataSource.connectionName));
    },function errorFunc(err) {
        deferred.reject(err);
    });

    return deferred.promise;
}

function documentExists(documentId, dataSourceOptions, checkOptions) {
    var deferred = q.defer();
    dataSourceOptions = initOptions(dataSourceOptions);
    getDataSource(dataSourceOptions).then(function (dataSource) {
        deferred.resolve(insertService.documentExists(documentId, dataSource.connectionName, checkOptions));
    },function errorFunc(err) {
        deferred.reject(err);
    });

    return deferred.promise;
}

function initOptions(options){
    if (!options) {
        options = {};
    }
    return options;
}

const undestroy = (id, userId, options) => {

    let deferred = q.defer();
    options = initOptions(options);
    getDataSource(options).then(function (dataSource) {
        deferred.resolve(deleteService.undestroy(id, userId, options));
    },function errorFunc(err) {
        deferred.reject(err);
    });

    return deferred.promise;

};

////////////////////////////////////
// public api
var DataService = {
    /**
     * initialise the data service
     */
    initialise: initialise,

    /**
     * close the data service
     */
    close: close,

    findOne: findOne,
    
    findTemplate,
    /**
     findAll - find all records, with optional query string
     parameters:
     query - a mongodb query to run. This can be any mongodb query
     userId - the userId for the operation. This needs to be the id of an account
     sortQuery - sort query to apply to the results
     allVersions - whether to retrieve all versions
     options - various options. Any new parameters should be added under options, unless it is felt that they are important enough
     to add a new parameter. Current options are:

         alternativeKey - alternative key to apply to query, eg. "Offline", instead of "Read".
         This can be used for specific operations
         excludeMissingKeys - documents with no keys will be excluded from the search. The default is to treat documents with
         no keys as accessible, but in certain operations like getting the offline documents you don't
         want documents with no keys
     */
    findAll: findAll,


    findVersions: findVersions,

    /*
     insert - insert a record
     parameters:
     user - ''
     template - ''
     */
    insert: insert,

    /*
     update - update an existing record, this only updates the fields specified in the updates.
     It won't overwrite others but can't handle complex updates
     parameters:
     id - the id of the record
     recordToUpdate - the object containing the updates to the record
     */
    update: update,

    /*
     jsonPatchUpdate - update an existing record. This accepts more complex updates like deleting fields,
     updating deep nested fields, or updating arrays. The updates must be passed
     through in the format returned by the npm jsonPatch module
     parameters:
     id - the id of the record
     recordToUpdate - the object containing the updates to the record
     */
    deepDiffUpdate: jsonPatchUpdate,

    jsonPatchUpdate: jsonPatchUpdate,

    /*
     destroy - delete a record. This is actually an update to set the status to Deleted
     parameters:
     id - the id of the record
     */
    destroy: destroy,

    getTemplate: getTemplate,

    documentExists: documentExists,

    undestroy: undestroy
};

module.exports = DataService;

