'use strict';

var q = require("q"),
    utilHttpStatus = require('../../shared/utils/UtilHttpStatus'),
    utilType = require('../../shared/utils/UtilType'),
    sharedConstants = require('../../shared/constants/SharedConstants'),
    Handlebars = require('handlebars'),
    logger = require('../utils/Logger').loggers.get('ftSearch'),
    UtilElasticSearch = require('../utils/UtilElasticSearch'),
    elasticConnection  = require('../search/ElasticConnection'),
    ESQueryUtils  = require('../../shared/utils/ESQueryUtils'),
    utilString = require('../../shared/utils/UtilString'),
    utilHandelbars = require('../../shared/utils/UtilHandlebars'),
     _ = require('lodash/lang'),
    request = require('request'),
    accountDataService;


const ServerAccessManager = require('../database/utils/ServerAccessManager');

utilHandelbars.registerHelper(Handlebars);

function createQueryWithNoFiltering(txt, fuzzyPrefixLength, templateFilter, queryString, sourceFilter) {
    const escaped = utilString.escapeSpecialChar(txt);
    // ~0070575, ~0070076, ~0070082 also split with dot, vertical bar, and dash
    const textQueries = escaped.split(/[^a-zA-Z\d:\u00C0-\u00FF]/g)
        .filter(text => (text !== '' && text !== ' ' && text !== '\\'))
        .map(text => ({
            "term": {
                "system_text_search": text.toLowerCase()
            }
    }));

    var finalQuery =  { "query": { "bool": {} } };

    if (templateFilter.query) {
        finalQuery.query.bool.filter = [...textQueries, templateFilter.query];
    }

    if (sourceFilter) {
        queryString["_source"] = sourceFilter;
    }

    return applyBoolToQueryString(finalQuery.query.bool, queryString);
}

const filterByApplication = (queryString, user) => {
    if (!queryString.query.bool.filter) {
        queryString.query.bool.filter = [];
    }

    var filter = { "bool":{ "must": [] } };

    if (user.applicationId) {
        filter.bool.must.push({ "terms": { "systemHeader.applicationIds": [ user.applicationId ] } });
    }

    if (user.tagApplicationId) {
        filter.bool.must.push({ "terms": { "systemHeader.tagApplicationId":  [ user.tagApplicationId ] } });
    }

    if (user.branchApplicationId) {
        filter.bool.must.push({ "terms": { "systemHeader.branchApplicationIds": [ user.branchApplicationId ] } });
    }

    queryString.query.bool.filter.push(filter);
    return queryString;
};

function filterResults(queryString, user, includeAllVersions, includeDeleted, searchOptions) {
    if (!queryString.query.bool.filter) {
        queryString.query.bool.filter = [];
    }

    var filter = {
        "bool":{
            "must": [],
            "must_not": [],
            "should": [],
            "minimum_should_match": 1 //require at least one should matched in the documents
        }
    };

    var should = [];

    // this is needed so documents without keyIds can be fetched
    var missingKeyIds = { "bool": {}};
    missingKeyIds.bool["must_not"] = { "exists": { "field": "systemHeader.keyIds" } };
    should.push(missingKeyIds);

    // this is needed so documents with empty keyIds can be fetched
    should.push({ "terms": { "systemHeader.keyIds": [] } });

    if (!includeAllVersions) {
        var currentVersion = {term: {}};
        currentVersion.term["systemHeader.currentVersion"] = true;
        filter.bool.must.push(currentVersion);
    }

    if (!includeDeleted) {
        var mustNotDeleted = {"term": {}};
        mustNotDeleted.term["systemHeader.deleted"] = true;
        filter.bool["must_not"].push(mustNotDeleted);
    }

    //account Security document should be removed from search result
    var mustNotAccountSecurity = {"term": {}};
    mustNotAccountSecurity.term["systemHeader.systemType"] = SharedConstants.SYSTEM_TYPE_ACCOUNT_SECURITY;
    filter.bool["must_not"].push(mustNotAccountSecurity);

    if (searchOptions && searchOptions.targetOperation === SharedConstants.OPERATION_TYPE_EXECUTE) {

        // No filter for keyIds because the search service needs to return 403 error for consistent with DataService.
        // The caller need to check Execute on the result.
        queryString.query.bool.filter.push(filter);

        return Promise.resolve();

    } else {

        return ServerAccessManager.getUserAccessKeysForOperation(user.documentId, SharedConstants.OPERATION_TYPE_READ)
            .then(function (readAccessKeys) {

                if (readAccessKeys && readAccessKeys.length !== 0) {
                    should.push({ "terms": { "systemHeader.keyIds": readAccessKeys } });
                }

                return finalize();

            }).catch(function (err) {

                return finalize();

            });
    }


    //helper
    function finalize(){
        filter.bool.should = filter.bool.should.concat(should);
        queryString.query.bool.filter.push(filter);
        return queryString;
    }

}

var applyBoolToQueryString = function (queryBool, queryString) {
    queryString.query.bool.filter.push({"bool":queryBool});
    return queryString;
};

var exceedsMaxQueryLength = query => {
    var maxSearchQueryLength = config.get("maxSearchQueryLength") ||
        sharedConstants.DEFAULT_MAX_SEARCH_QUERY_LENGTH;

    return JSON.stringify(query).length > maxSearchQueryLength;
};

const generatePit = () => {
    const esConfig = UtilElasticSearch.searchConfig();
    return new Promise((resolve, reject) => {
        request.post(`http://${esConfig.host}/${esConfig.indexName}/_pit?keep_alive=1m`,
            null,
            function (error, response, body) {
                if (!error && response.statusCode === 200) {
                    resolve(JSON.parse(body));
                } else {
                    reject(new Error('Error generating pit: ' + error.message));
                }
            }
        );
    });
};

var esClient;

const search = (query, userId, allVersions, searchOptions) =>{
    const esConfig = UtilElasticSearch.searchConfig(searchOptions.connectionName);
    if (!esClient) {
        esClient = elasticConnection.esConnection(esConfig);
    }

    accountDataService = require('../database/dataServices/AccountService');

    var includeAllVersions = false;
    var includeDeleted = false;
    var deferred = q.defer();

    var txt = query.text;

    var size = query.size || query.pageSize || sharedConstants.DEFAULT_PAGE_SIZE;
    if (query.size === 0) {
        size = 0;
    }

    var page = query.page || sharedConstants.DEFAULT_PAGE;
    var from = query.from;
    var trackTotalHits = query.trackTotalHits || false;

    var sort = query.sort;
    var searchAfter = query.searchAfter;
    var pit = query.pit;

    var searchProviderOptions = query.searchProviderOptions || {};

    var sourceFilter = query.sourceFilter;
    var templateFilter = {};
    var results = {
        statusCode: utilHttpStatus.OK,
        data: [],
        filter: false
    };

    if (query.includeAllVersions) {
        allVersions = query.includeAllVersions;
    }

    let convertTotalToES2Format = true;
    if (esConfig.convertTotalToES2Format !== undefined) {
        convertTotalToES2Format = esConfig.convertTotalToES2Format;
    }

    if (query.convertTotalToES2Format !== undefined) {
        convertTotalToES2Format = query.convertTotalToES2Format;
    }

    //#11535 - includeAllVersions should accept boolean, string, and int
    if (allVersions && (
        allVersions === true
        || allVersions === "true"
        || allVersions === 1
        || allVersions === "1")) {
        includeAllVersions = true;
    }

    if (query.includeDeleted) {
        includeDeleted = query.includeDeleted;
    }

    if (query.filter && exceedsMaxQueryLength(query.filter)) {
        var message = "The maximum length of a search query has been exceeded.";
        logger.error(message);

        return Promise.reject(new Error(message));
    }

    accountDataService.getAccountById(userId).then(function successFunc(user) {
        try {
            //filters
            //------------------------------------------------------------//
            if (query.filter) {
                if(utilType.isString(query.filter)) {
                    var queryFilter = query.filter.replace(/'/g, '"');

                    if (query.document) {

                        var document = utilType.isString(query.document) ? JSON.parse(query.document) : query.document;

                        var doc = {
                            document: document,
                            account: user
                        };

                        var hbTemplate = Handlebars.compile(queryFilter);
                        queryFilter = hbTemplate(doc);
                    }

                    templateFilter = utilType.isString(queryFilter) ? JSON.parse(queryFilter) : queryFilter;
                } else {
                    //in case of filter is a json object not a string
                    templateFilter = query.filter;
                }
            } else {

                query.filter = {"bool":{
                    "must":[],
                    "must_not": [ {"term" : {"systemHeader.excludeGeneralSearch" : true}} ],
                    "should": [],
                    "minimum_should_match": 1 //require at least one should matched in the documents
                }};

                if (!includeDeleted) {
                    var systemDeleted = {"term": {}};
                    systemDeleted.term["systemHeader.deleted"] = true;
                    query.filter.bool["must_not"].push(systemDeleted);
                }

                templateFilter.query = query.filter;
            }

            //------------------------------------------------------------//

            var fuzzyPrefixLength = 0;

            if (esConfig && esConfig.fuzzyPrefixLength) {
                fuzzyPrefixLength = esConfig.fuzzyPrefixLength;
                logger.info("ElasticSearch using fuzzy prefix length of " + fuzzyPrefixLength);
            }

            var queryString = {
                "query": {
                    "bool": { "filter": [] }
                }
            };

            if(templateFilter.sort) {
                queryString.sort = templateFilter.sort || sort;
            }

            if (searchAfter) {
                queryString['search_after'] = searchAfter;
            }

            if (pit) {
                queryString.pit = pit;
            }

            for (const key in searchProviderOptions) {
                queryString[key] = searchProviderOptions[key];
            }

            if (txt){
                var jsonIndx = txt.search(/{/i);
                if (jsonIndx < 0) {
                    if (query.queryToBeFiltered !== 'null' && query.queryToBeFiltered) {
                        var queryJson = JSON.parse(query.queryToBeFiltered);
                        var qf = {"query": queryJson.filter};
                        txt = utilString.escapeSpecialChar(txt);

                        var term = {"term" : {"systemHeader.excludeGeneralSearch" : false}} ;

                        queryString.query.bool.filter.push({
                            "bool": {
                                "must": [
                                    {
                                        "query": {
                                            "queryString": {
                                                "query": txt + " *",
                                                "default_operator": "AND",
                                                "fuzzy_prefix_length": 0
                                            }
                                        }
                                    },
                                    term
                                ]
                            }
                        });

                        if (qf.query.match) {
                            queryString.query.bool.filter[0].bool.must.push({ "match": qf.query.match });
                        } else if (qf.query.bool) {
                            queryString = applyBoolToQueryString(qf.query.bool, queryString);
                        }

                        if (sourceFilter) {
                            queryString["_source"] = sourceFilter;
                        }

                    } else {
                        queryString = createQueryWithNoFiltering(txt, fuzzyPrefixLength, templateFilter,
                            queryString, sourceFilter);
                    }
                } else {
                    var jsonTxt = JSON.parse(txt);
                    var newQuery = {
                        "query": {
                            "bool": {
                                "must": [],
                                "must_not": [],
                                "should": [],
                                "filter": []
                            }
                        }
                    };

                    if (jsonTxt.filter) {
                        results.filter = true;
                        newQuery.query.bool.must.push(jsonTxt.filter);

                        queryString = newQuery;
                    } else if (jsonTxt.query.bool) {
                        newQuery = applyBoolToQueryString(jsonTxt.query.bool, newQuery);
                        queryString = newQuery;
                    } else {
                        queryString = jsonTxt;
                    }
                }
            }
            else if (templateFilter) {
                if (templateFilter.query.match) {
                    var filter = {"bool": { "must": [{ "match": templateFilter.query.match }]}};
                    queryString.query.bool.filter.push(filter);
                } else if (templateFilter.query.bool) {
                    queryString = applyBoolToQueryString(templateFilter.query.bool, queryString);
                } else {
                    queryString.query.bool = templateFilter;
                }

                if (templateFilter.size)
                    size = templateFilter.size;

                if (templateFilter.from)
                    from = templateFilter.from;
            }

            filterByApplication(queryString, user);

            filterResults(queryString, user, includeAllVersions, includeDeleted, searchOptions).then(function (result) {

                // add the source filter to the search
                if (sourceFilter) {
                    queryString["_source"] = sourceFilter;
                }

                if (trackTotalHits) {
                    queryString["track_total_hits"] = true;
                }

                var elasticIndexName = UtilElasticSearch.elasticIndexName();

                if (!elasticIndexName) {
                    var err = "Error: Misconfigured elastic search!";
                    logger.error(err);
                    deferred.reject(err);
                } else {
                    if (!from) from = (parseInt(page)) * size;

                    var options = {
                        "index": elasticIndexName,
                        "from": from,
                        "size": size,
                        "body": queryString
                    };

                    logger.info('Elastic Search Query: ' + JSON.stringify(queryString));

                    esClient.search(options, function (err, result) {
                        let data = result && result.body ? result.body : {};
                        if (err) {
                            var message = err;
                            if (data && data.error && data.error["root_cause"][0]) {
                                message = data.error["root_cause"][0].reason;
                            }

                            var error = {
                                errorCode: utilHttpStatus.BAD_REQUEST,
                                message: message
                            };
                            logger.error("Error on elastic search : " + JSON.stringify(err));
                            deferred.reject(error);
                        }
                        else {
                            // LOOPING THROUGH THE RESULTS IMPACTS PERFORMANCE OF THE SEARCH DRASTICALLY.
                            // ANY FILTERING SHOULD BE DONE IN THE SEARCH QUERY SENT TO ELASTICSEARCH,
                            // SO NO LOOPING SHOULD BE ADDED HERE

                            //from elastic7, hits.total value has been changed as object
                            if (convertTotalToES2Format && _.isObject(data.hits.total)){
                                data.hits.total = data.hits.total.value;
                            }

                            results.data = data;
                            results.statusCode = utilHttpStatus.OK;

                            deferred.resolve(results);
                        }
                    });
                }

            }).catch(function (e) {
                var msg = 'Error: ' + e;
                logger.error(msg);
                deferred.reject(msg);
            });

        } catch(e) {
            var msg = 'Error: ' + e;
            logger.error(msg);
            deferred.reject(msg);
        }
    }, function errorFunc(err) {
        var msg = "Error getting user: " + err;
        logger.error(msg);
        deferred.reject(msg);
    });

    return deferred.promise;
};

const getFindDocumentsQuery = (query) =>{
  var qry = utilType.isString(query) ? JSON.parse(query) : query;

  var eqry = {"query": {"bool": {"filter": []}}};
  for (var key in qry) {
    let term = {};
    term[key] = qry[key];
    eqry.query.bool.filter.push({"term" : term});
  }

  return {"filter" : eqry};
};

const getDoFindAllQuery = (query) =>{
  var esQuery = {'filter':{'query':{'bool':{'must':[]}}}};
  for (let key in query) {
    let term = {};
    term[key] = query[key];
    esQuery.filter.query.bool.must.push({"term" : term });
  }

  return esQuery;
};

module.exports = {
  search: search,

  getFindDocumentsQuery : getFindDocumentsQuery,

  getDoFindAllQuery : getDoFindAllQuery,

  generatePit

};
