"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("./models");
const fetch = require("isomorphic-fetch");
const fs_1 = require("fs");
const path = require("path");
class AbstractSearchService {
    /**
     *
     * @param options defines the options for the search service
     */
    constructor(options) {
        this.rawData = [];
        this.synonymsList = [];
        /**
         * push different methods to this empty array
         * to run methods after cache has been updated
         */
        this.serverDataCallBacks = [];
        this.options = options;
    }
    /**
     * sets up the intetvall for refetching the data from the AEM
     */
    setUpCacheInterval() {
        return __awaiter(this, void 0, void 0, function* () {
            console.info(`${this.options.serviceName} instance ${this.instanceNumber} will update the cache every ${this.options.cacheTime} minutes`);
            setInterval(() => __awaiter(this, void 0, void 0, function* () {
                yield this.cacheIntervalLifecycle();
            }), 1000 * 60 * this.options.cacheTime);
        });
    }
    cacheIntervalLifecycle() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.cacheIntervalBefore();
            yield this.triggerGetServerData();
            yield this.cacheIntervalAfter();
        });
    }
    /**
     * async method which will automatically get called
     * by index.ts
     */
    setUpSearchService() {
        return __awaiter(this, void 0, void 0, function* () {
            // for multi instance handling every new instance get's a delay of 5 minutes so fetching doesn't kill the server
            if (this.instanceNumber) {
                console.info(`${this.options.serviceName} instance ${this.instanceNumber} will start fetching with a delay of ${this.instanceNumber * this.instanceDelay} minutes`);
                setTimeout(() => __awaiter(this, void 0, void 0, function* () {
                    this.setUp();
                }), 1000 * 60 * (this.instanceDelay * this.instanceNumber));
            }
            else {
                console.info(`${this.options.serviceName} instance ${this.instanceNumber} will start now.`);
                this.setUp();
            }
        });
    }
    setUp() {
        return __awaiter(this, void 0, void 0, function* () {
            // call custom methods
            yield this.setUpCallbacksBefore();
            //call set up methods
            this.setUpDefaultAPI();
            this.setUpCacheInterval();
            this.synonymsList = this.getDefaultSynonyms();
            // call custom methods
            yield this.setUpCallbacksAfter();
        });
    }
    /**
     * creates a default rest endpoint for the search when express and a apiPath is set
     */
    setUpDefaultAPI() {
        if (this.options.express && this.options.express.app) {
            this.options.express.app.get(`${this.options.express.apiPath}`, (req, res) => {
                const search = req.query.search;
                console.info(`Looking for search result for "${search}" ${new Date().toISOString()}`);
                res.send(this.getSearchResult(search));
                console.info(`Send response for search result "${search}" ${new Date().toISOString()}`);
            });
            this.options.express.app.get(`${this.options.express.apiPath}/updateCache`, (req, res) => {
                console.info(`Trying to trigger manual cache update`);
                let response = "Manual cache update disabled";
                if (this.enableCacheTrigger) {
                    this.cacheIntervalLifecycle();
                    response = `Manual cache update triggered`;
                }
                console.info(response);
                res.send(response).status(200);
            });
            this.options.express.app.get(`${this.options.express.apiPath}/lastCacheUpdate`, (req, res) => {
                console.info(`Returning last cache update`);
                let response = "Manual cache update disabled";
                if (this.enableCacheTrigger) {
                    response = `Last cache update at: ${this.lastCacheUpdate.time} - Status: ${this.lastCacheUpdate.status}`;
                }
                console.info(response);
                res.send(response).status(200);
            });
            this.options.express.app.get(`${this.options.express.apiPath}/synonyms`, (req, res) => {
                const searchWord = req.query.word;
                console.info(`Looking for synonyms of word "${searchWord}"`);
                res.send(this.getSynonymsOfWord(searchWord));
                console.info(`Send response for synonyms of word "${searchWord}" ${new Date().toISOString()}`);
            });
        }
        else {
            console.info("No default API will be created");
        }
    }
    /**
     * this method is used to manually trigger the cache update
     */
    triggerGetServerData() {
        return __awaiter(this, void 0, void 0, function* () {
            console.info(`Update Cache of ${this.options.serviceName}`, new Date().toISOString());
            try {
                yield this.getServerData();
                console.info(`Finish loading cache ${new Date().toISOString()}`);
                for (const fn of this.serverDataCallBacks) {
                    yield fn();
                }
                console.info(`Finish serverDataCallbacks() ${new Date().toISOString()}`);
            }
            catch (e) {
                console.error(`${this.options.serviceName} - ${new Date().toISOString()} - triggerGetServerData() =>`, e);
            }
        });
    }
    /**
     * fetches the raw data from the (AEM) server.
     * This will be done in intervall depending on the cache config to update the cached data.
     * The fetched data needs  structure like: {pathList:[{...}]}
     */
    getServerData(serverDataRequestConfig) {
        return __awaiter(this, void 0, void 0, function* () {
            let data;
            let responseText;
            let response;
            const urls = this.getSearchUrl(serverDataRequestConfig);
            const searchUrls = Array.isArray(urls)
                ? urls
                : [...urls];
            let isValidResponse = false;
            let searchUrl = "";
            for (let urlIndex in searchUrls) {
                const url = searchUrls[urlIndex];
                try {
                    console.info(`Start fetching URL nr. ${urlIndex} with ${url} -  ${new Date().toISOString()}`);
                    response = yield fetch(url, this.getSearchUrlRequestInits());
                    responseText = yield response.text();
                    data = JSON.parse(responseText || "{}");
                    console.info(`Done fetching URL nr. ${urlIndex} with ${url} - ${new Date().toISOString()}`);
                    searchUrl = url;
                }
                catch (e) {
                    console.warn(`Error while fetching URL nr. ${urlIndex} with ${url} -  ${new Date().toISOString()}`, responseText, e);
                }
                isValidResponse = data && data.pathList && data.pathList.length > 0;
                if (isValidResponse) {
                    break;
                }
            }
            if (isValidResponse) {
                if (!serverDataRequestConfig || !this.rawData) {
                    this.rawData = [];
                }
                data.pathList.forEach((startChild) => { var _a; return this.setUpData(startChild, (_a = serverDataRequestConfig) === null || _a === void 0 ? void 0 : _a.type); });
                console.info(`fetching ${this.options.serviceName} - ${searchUrl} data done`);
                this.lastCacheUpdate = {
                    time: new Date().toISOString(),
                    status: models_1.Status.Success,
                };
            }
            else {
                console.warn(`fetching ${this.options.serviceName} - ${JSON.stringify(urls)} data failed!`, data);
                this.lastCacheUpdate = {
                    time: new Date().toISOString(),
                    status: models_1.Status.Error,
                };
            }
            return;
        });
    }
    /**
     * takes a string with one or multiple words and return a SearchResultModel
     * @param search a search string with multiple worlds like: 'imprint page'
     */
    getSearchResult(search) {
        if (!search) {
            return { search: "not-definied", foundItems: 0, results: [] };
        }
        search = search.trim().toLowerCase();
        const hasMultipleWords = search.split(" ").length > 1;
        const firstLevelSearch = this.searchForSingleWord(search);
        if (firstLevelSearch.foundItems === 0 && hasMultipleWords) {
            // search for every word
            const secondSearchTerm = search
                .split(" ")
                .sort((a, b) => b.length - a.length);
            for (const _search of secondSearchTerm) {
                const secondLevelSearch = this.searchForSingleWord(_search);
                if (secondLevelSearch.foundItems > 0) {
                    console.info("used second", _search);
                    return secondLevelSearch;
                }
            }
        }
        return firstLevelSearch;
    }
    /**
     * takes a string with one word and return a SearchResultModel
     * @param search a search string with only one word like: 'imprint'
     */
    searchForSingleWord(search) {
        const synonyms = this.getSynonyms(search);
        const results = (search
            ? this.rawData
                .map((searchElement) => this.getSearchRank(searchElement, synonyms))
                .filter((searchElement) => searchElement.searchRank > 0)
                .sort((a, b) => b.searchRank - a.searchRank)
            : this.rawData) || [];
        return { search: synonyms.toString(), foundItems: results.length, results };
    }
    /**
     * filters the raw content child for the needed content keys. Injected manipulation method can be used to change
     * the content
     * @param contentChild a content child from the raw data of the (AEM) server api
     * @param pageContentKeys a list of strings which are used as keys in the content child to reduce the raw data
     */
    getFilteredContent(contentChild, pageContentKeys) {
        const { _jcrContent } = contentChild;
        const content = this.getObjectKeys(_jcrContent, pageContentKeys);
        return content;
    }
    getObjectKeys(obj, pageContentKeys) {
        let mappedKeys = {};
        if (obj) {
            pageContentKeys.forEach((config) => {
                const val = obj[config.key];
                if (val) {
                    if (Array.isArray(val)) {
                        mappedKeys[config.key] = config.manipulation
                            ? val.map((el) => config.manipulation(val))
                            : val;
                    }
                    else if (typeof val === "object") {
                        mappedKeys[config.key] = this.getObjectKeys(val, pageContentKeys);
                    }
                    else {
                        mappedKeys[config.key] = config.manipulation
                            ? config.manipulation(val)
                            : val;
                    }
                }
            });
        }
        return mappedKeys;
    }
    /**
     * looks for synonyms by a given string and reteturns the used string with the list of synonyms
     * @param search a single world string
     */
    getSynonyms(search) {
        let synonyms = [search.toLowerCase()];
        this.synonymsList.forEach((list) => {
            const isSynonym = list.indexOf(search) > -1;
            // make sure all synonyms are to lowercase first
            if (isSynonym) {
                synonyms = [...new Set([...synonyms, ...list])];
            }
        });
        return synonyms;
    }
    /**
     * takes a string (possibly separated by ,)
     * and returns a json Object with all synonyms for each word
     * @param searchWord
     */
    getSynonymsOfWord(searchWord) {
        let results = {};
        let wordArray = searchWord.split(",");
        wordArray.forEach((word) => {
            const synonyms = this.synonymsList.filter((synonymList) => {
                return synonymList.find((synonym) => synonym.toLowerCase() === word.toLowerCase());
            });
            results[word] = synonyms;
        });
        return results;
    }
    // TODO rank single words higher: eis => eis > reis
    // TODO add tests
    /**
     * get the searchrank for a search or list of synonyms
     * @param searchModel
     * @param searchWithSynonyms
     */
    getSearchRank(searchModel, searchWithSynonyms) {
        const criteria = this.getSearchCriteria(searchModel) || [];
        // 0 indicates that there is no search result in that product
        searchModel.searchRank = 0;
        criteria.forEach((el) => {
            if (this.isKeyWordFoundInSynonym(el, searchWithSynonyms)) {
                // multiple hits sum up the search rank
                searchModel.searchRank = searchModel.searchRank + el.rank;
            }
        });
        return searchModel;
    }
    /**
     * checks if a query or it's synonym is in a given search model
     * @param searchModel an elements of the search model with a search rank
     * @param search the query of the search from a user
     */
    isKeyWordFoundInSynonym(searchModel, searchWithSynonyms) {
        if (!searchModel || !searchModel.searchElement) {
            return false;
        }
        let isFound = false;
        const isArray = Array.isArray(searchModel.searchElement);
        // test if one of the synonyms is in the search model
        for (const synonym of searchWithSynonyms) {
            try {
                const found = isArray
                    ? searchModel.searchElement.findIndex((searchElement) => {
                        return this.matchKeywordToSynonym(synonym, searchElement, searchModel.fullMatch);
                    }) > -1
                    : this.matchKeywordToSynonym(synonym, searchModel.searchElement, searchModel.fullMatch);
                if (found) {
                    isFound = true;
                    // no need for more iterations if the search is found
                    break;
                }
            }
            catch (e) {
                console.warn(`${this.options.serviceName} - foundKeyWordOrSynonym()`, e);
            }
        }
        return isFound;
    }
    matchKeywordToSynonym(synonym, searchElement, fullMatch) {
        if (fullMatch) {
            return ((searchElement || "").toLowerCase() === (synonym || "").toLowerCase());
        }
        else {
            return (searchElement || "")
                .toLowerCase()
                .includes((synonym || "").toLowerCase());
        }
    }
    /**
     *
     * @param data an array of raw data elements from the (AEM) server API
     */
    setUpData(data, type) {
        if (!data) {
            console.warn(`setUpData failed for ${this.options.serviceName}`);
            return;
        }
        data.forEach((child) => {
            if (child.children) {
                this.rawData.push(this.getRawDataElement(child, type));
                this.setUpData(child.children, type);
            }
        });
    }
    /**
     * returns a german synonym list
     */
    getDefaultSynonyms() {
        const rawText = fs_1.readFileSync(path.resolve(__dirname, "./lib/synonyms.txt"), "utf-8").toString();
        const parsedSynonyms = rawText.split("\n").map((line) => line.split(";").map((synonym) => synonym
            .replace(/\(([^)]+)\)/, "")
            .replace("  ", " ")
            .trim()
            .toLowerCase()));
        console.info(`Loaded synonym list with ${parsedSynonyms.length} synonyms`);
        return parsedSynonyms;
    }
}
exports.AbstractSearchService = AbstractSearchService;
//# sourceMappingURL=AbstractSearch.service.js.map