"use strict";
/*****************************************************************************\
|                                               ( )_  _                       |
|    _ _    _ __   _ _    __    ___ ___     _ _ | ,_)(_)  ___   ___     _     |
|   ( '_`\ ( '__)/'_` ) /'_ `\/' _ ` _ `\ /'_` )| |  | |/',__)/' v `\ /'_`\   |
|   | (_) )| |  ( (_| |( (_) || ( ) ( ) |( (_| || |_ | |\__, \| (˅) |( (_) )  |
|   | ,__/'(_)  `\__,_)`\__  |(_) (_) (_)`\__,_)`\__)(_)(____/(_) (_)`\___/'  |
|   | |                ( )_) |                                                |
|   (_)                 \___/'                                                |
|                                                                             |
| General Bots Copyright (c) Pragmatismo.io. All rights reserved.             |
| Licensed under the AGPL-3.0.                                                |
|                                                                             |
| According to our dual licensing model, this program can be used either      |
| under the terms of the GNU Affero General Public License, version 3,        |
| or under a proprietary license.                                             |
|                                                                             |
| The texts of the GNU Affero General Public License with an additional       |
| permission and of our proprietary license can be found at and               |
| in the LICENSE file you have received along with this program.              |
|                                                                             |
| This program is distributed in the hope that it will be useful,             |
| but WITHOUT ANY WARRANTY, without even the implied warranty of              |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                |
| GNU Affero General Public License for more details.                         |
|                                                                             |
| "General Bots" is a registered trademark of Pragmatismo.io.                 |
| The licensing of the program under the AGPLv3 does not imply a              |
| trademark license. Therefore any rights, title and interest in              |
| our trademarks remain entirely with us.                                     |
|                                                                             |
\*****************************************************************************/
Object.defineProperty(exports, "__esModule", { value: true });
exports.KBService = exports.KBServiceSearchResults = void 0;
/**
 * @fileoverview Knowledge base services and logic.
 */
const Path = require('path');
const Fs = require('fs');
const urlJoin = require('url-join');
const path = require('path');
const asyncPromise = require('async-promises');
const walkPromise = require('walk-promise');
// tslint:disable-next-line:newline-per-chained-call
const { SearchService } = require('azure-search-client');
const Excel = require('exceljs');
const getSlug = require('speakingurl');
const app_1 = require("../../../src/app");
const botlib_1 = require("botlib");
const pragmatismo_io_framework_1 = require("pragmatismo-io-framework");
const sequelize_1 = require("sequelize");
const AzureDeployerService_1 = require("../../azuredeployer.gbapp/services/AzureDeployerService");
const GBModel_1 = require("../../core.gbapp/models/GBModel");
const CSService_1 = require("../../customer-satisfaction.gbapp/services/CSService");
const models_1 = require("../models");
const GBConfigService_1 = require("./../../core.gbapp/services/GBConfigService");
/**
 * Result for quey on KB data.
 */
class KBServiceSearchResults {
}
exports.KBServiceSearchResults = KBServiceSearchResults;
/**
 * All services related to knowledge base management.
 */
class KBService {
    constructor(sequelize) {
        this.sequelize = sequelize;
    }
    static getFormattedSubjectItems(subjects) {
        if (subjects === null) {
            return '';
        }
        const out = [];
        subjects.forEach(subject => {
            out.push(subject.title);
        });
        return out.join(', ');
    }
    static getSubjectItemsSeparatedBySpaces(subjects) {
        const out = [];
        if (subjects === undefined) {
            return '';
        }
        subjects.forEach(subject => {
            out.push(subject.internalId);
        });
        return out.join(' ');
    }
    async getAnswerTextByMediaName(instanceId, answerMediaName) {
        const answer = await models_1.GuaribasAnswer.findOne({
            where: {
                instanceId: instanceId,
                media: answerMediaName
            }
        });
        return answer != undefined ? answer.content : null;
    }
    async getQuestionById(instanceId, questionId) {
        return models_1.GuaribasQuestion.findOne({
            where: {
                instanceId: instanceId,
                questionId: questionId
            }
        });
    }
    async getAnswerById(instanceId, answerId) {
        return models_1.GuaribasAnswer.findOne({
            where: {
                instanceId: instanceId,
                answerId: answerId
            }
        });
    }
    /**
     * Returns a question object given a SEO friendly URL.
     */
    async getQuestionIdFromURL(core, url) {
        // Extracts questionId from URL.
        const id = url.substr(url.lastIndexOf('-') + 1);
        // Extracts botId from URL.
        let path = /(http[s]?:\/\/)?([^\/\s]+\/)(.*)/gi;
        const botId = url.replace(path, ($0, $1, $2, $3) => {
            return $3.substr($3.indexOf('/'));
        });
        // Finds the associated question.
        const instance = await core.loadInstanceByBotId(botId);
        const question = await models_1.GuaribasQuestion.findAll({
            where: {
                instanceId: instance.instanceId,
                questionId: id
            }
        });
        return question;
    }
    async getQuestionsSEO(instanceId) {
        const questions = await models_1.GuaribasQuestion.findAll({
            where: {
                instanceId: instanceId
            }
        });
        let output = [];
        for (let i = 0; i < questions.length; i++) {
            const answer = questions[i];
            const text = getSlug(answer.content);
            let url = `${text}-${i}`;
            output.push(url);
        }
        return output;
    }
    async getAnswerByText(instanceId, text) {
        text = text.trim();
        const service = new CSService_1.CSService();
        let question = await service.getQuestionFromAlternateText(instanceId, text);
        if (!question) {
            question = await models_1.GuaribasQuestion.findOne({
                where: {
                    instanceId: instanceId,
                    content: { [sequelize_1.Op.like]: `%${text}%` }
                }
            });
        }
        if (!question) {
            question = await models_1.GuaribasQuestion.findOne({
                where: {
                    instanceId: instanceId,
                    content: { [sequelize_1.Op.eq]: `${text}` }
                }
            });
        }
        if (question !== null) {
            const answer = await models_1.GuaribasAnswer.findOne({
                where: {
                    instanceId: instanceId,
                    answerId: question.answerId
                }
            });
            return { question: question, answer: answer };
        }
        return undefined;
    }
    async addAnswer(obj) {
        return await models_1.GuaribasAnswer.create(obj);
    }
    async ask(instance, query, searchScore, subjects) {
        // Builds search query.
        query = query.toLowerCase();
        query = query.replace('?', ' ');
        query = query.replace('!', ' ');
        query = query.replace('.', ' ');
        query = query.replace('/', ' ');
        query = query.replace('\\', ' ');
        query = query.replace('\r\n', ' ');
        // Try simple search first.
        const data = await this.getAnswerByText(instance.instanceId, query.trim());
        if (data) {
            botlib_1.GBLog.info(`Simple SEARCH called.`);
            return { answer: data.answer, questionId: data.question.questionId };
        }
        if (subjects !== null) {
            const text = KBService.getSubjectItemsSeparatedBySpaces(subjects);
            if (text !== null) {
                query = `${query} ${text}`;
            }
        }
        // No direct match found, so Search is used.
        if (instance.searchKey !== null && GBConfigService_1.GBConfigService.get('STORAGE_DIALECT') === 'mssql') {
            const client = new SearchService(instance.searchHost.split('.')[0], instance.searchKey);
            const results = await client.indexes
                .use(instance.searchIndex)
                .buildQuery()
                .filter(f => f.eq('instanceId', instance.instanceId))
                .filter(f => f.eq('skipIndex', false))
                .search(query)
                .top(1)
                .executeQuery();
            const values = results.result.value;
            let returnedScore = 0;
            // Searches via Search (Azure Search).
            if (values && values.length > 0) {
                returnedScore = values[0]['@search.score'];
                if (returnedScore >= searchScore) {
                    const value = await this.getAnswerById(instance.instanceId, values[0].answerId);
                    if (value !== null) {
                        botlib_1.GBLog.info(`SEARCH WILL BE USED with score: ${returnedScore} > required (searchScore): ${searchScore}`);
                        return { answer: value, questionId: values[0].questionId };
                    }
                    else {
                        botlib_1.GBLog.info(`SEARCH WILL NOT be used as answerId ${values[0].answerId} was not found in database,
                returnedScore: ${returnedScore} < required (searchScore): ${searchScore}`);
                        return { answer: undefined, questionId: 0 };
                    }
                }
                else {
                    botlib_1.GBLog.info(`SEARCH called but returned LOW level score,
              returnedScore: ${returnedScore} < required (searchScore): ${searchScore}`);
                    return { answer: undefined, questionId: 0 };
                }
            }
            else {
                botlib_1.GBLog.info(`SEARCH called but NO answer could be found (zero results).`);
                return { answer: undefined, questionId: 0 };
            }
        }
    }
    async getSubjectItems(instanceId, parentId) {
        const where = { parentSubjectId: parentId, instanceId: instanceId };
        return models_1.GuaribasSubject.findAll({
            where: where
        });
    }
    async getFaqBySubjectArray(instanceId, from, subjects) {
        if (subjects) {
            const where = {
                from: from,
                // tslint:disable-next-line: no-null-keyword
                subject1: null,
                // tslint:disable-next-line: no-null-keyword
                subject2: null,
                // tslint:disable-next-line: no-null-keyword
                subject3: null,
                // tslint:disable-next-line: no-null-keyword
                subject4: null,
                // tslint:disable-next-line: no-null-keyword
                instanceId: instanceId
            };
            if (subjects[0] && subjects[0].internalId) {
                where.subject1 = subjects[0].internalId;
            }
            if (subjects[1] && subjects[1].internalId) {
                where.subject2 = subjects[1].internalId;
            }
            if (subjects[2] && subjects[2].internalId) {
                where.subject3 = subjects[2].internalId;
            }
            if (subjects[3] && subjects[3].internalId) {
                where.subject4 = subjects[3].internalId;
            }
            return await models_1.GuaribasQuestion.findAll({
                where: where
            });
        }
        else {
            return await models_1.GuaribasQuestion.findAll({
                where: { from: from, instanceId: instanceId }
            });
        }
    }
    async importKbTabularFile(filePath, instanceId, packageId) {
        botlib_1.GBLog.info(`Now reading file ${filePath}...`);
        const workbook = new Excel.Workbook();
        const data = await workbook.xlsx.readFile(filePath);
        let lastQuestionId;
        let lastAnswer;
        // Finds a valid worksheet because Excel returns empty slots
        // when loading worksheets collection.
        let worksheet;
        for (let t = 0; t < data._worksheets.length; t++) {
            worksheet = data._worksheets[t];
            if (worksheet) {
                break;
            }
        }
        const rows = worksheet._rows;
        const answers = [];
        const questions = [];
        botlib_1.GBLog.info(`Processing ${rows.length} rows from tabular file ${filePath}...`);
        await asyncPromise.eachSeries(rows, async (line) => {
            // Skips the first line.
            if (line != undefined &&
                line._cells[0] !== undefined &&
                line._cells[1] !== undefined &&
                line._cells[2] !== undefined &&
                line._cells[3] !== undefined &&
                line._cells[4] !== undefined) {
                // Extracts values from columns in the current line.
                const subjectsText = line._cells[0].text;
                const from = line._cells[1].text;
                const to = line._cells[2].text;
                const question = line._cells[3].text;
                let answer = line._cells[4].text;
                if (!(subjectsText === 'subjects' && from === 'from') && answer !== null && question !== null) {
                    let format = '.txt';
                    // Extracts answer from external media if any.
                    let media = null;
                    if (typeof answer !== 'string') {
                        botlib_1.GBLog.info(`[GBImporter] Answer is NULL related to Question '${question}'.`);
                        answer =
                            'Existe um problema na base de conhecimento. Fui treinado para entender sua pergunta, avise a quem me criou que a resposta não foi informada para esta pergunta.';
                    }
                    else if (answer.indexOf('.md') > -1) {
                        const mediaFilename = urlJoin(path.dirname(filePath), '..', 'articles', answer);
                        if (Fs.existsSync(mediaFilename)) {
                            answer = Fs.readFileSync(mediaFilename, 'utf8');
                            format = '.md';
                            media = path.basename(mediaFilename);
                        }
                        else {
                            botlib_1.GBLog.info(`[GBImporter] File not found: ${mediaFilename}.`);
                            answer = '';
                        }
                    }
                    // Processes subjects hierarchy splitting by dots.
                    const subjectArray = subjectsText.split('.');
                    let subject1;
                    let subject2;
                    let subject3;
                    let subject4;
                    let indexer = 0;
                    subjectArray.forEach(element => {
                        if (indexer === 0) {
                            subject1 = subjectArray[indexer].substring(0, 63);
                        }
                        else if (indexer === 1) {
                            subject2 = subjectArray[indexer].substring(0, 63);
                        }
                        else if (indexer === 2) {
                            subject3 = subjectArray[indexer].substring(0, 63);
                        }
                        else if (indexer === 3) {
                            subject4 = subjectArray[indexer].substring(0, 63);
                        }
                        indexer++;
                    });
                    // Now with all the data ready, creates entities in the store.
                    const answer1 = {
                        instanceId: instanceId,
                        content: answer,
                        format: format,
                        media: media,
                        packageId: packageId,
                        prevId: lastQuestionId !== null ? lastQuestionId : 0
                    };
                    answers.push(answer1);
                    const question1 = {
                        from: from,
                        to: to,
                        subject1: subject1,
                        subject2: subject2,
                        subject3: subject3,
                        subject4: subject4,
                        content: question.replace(/["]+/g, ''),
                        instanceId: instanceId,
                        skipIndex: (question.charAt(0) === "\""),
                        packageId: packageId
                    };
                    questions.push(question1);
                    // TODO: Tutorial. if (lastAnswer !== undefined && lastQuestionId !== 0) {
                    //   await lastAnswer.update({ nextId: lastQuestionId });
                    // }
                    // lastAnswer = answer1;
                    // lastQuestionId = question1.questionId;
                    return true;
                }
                else {
                    // Skips the header.
                    return undefined;
                }
            }
        });
        const answersCreated = await models_1.GuaribasAnswer.bulkCreate(answers);
        let i = 0;
        await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(questions, async (question) => {
            question.answerId = answersCreated[i++].answerId;
        });
        return await models_1.GuaribasQuestion.bulkCreate(questions);
    }
    async sendAnswer(min, channel, step, answer) {
        if (answer.content.endsWith('.mp4')) {
            await this.playVideo(min, min.conversationalService, step, answer, channel);
        }
        else if (answer.content.endsWith('.ppt') ||
            answer.content.endsWith('.pptx') ||
            answer.content.endsWith('.doc') ||
            answer.content.endsWith('.docx') ||
            answer.content.endsWith('.xls') ||
            answer.content.endsWith('.xlsx')) {
            const doc = urlJoin(app_1.GBServer.globals.publicAddress, 'kb', `${min.instance.botId}.gbai`, `${min.instance.botId}.gbkb`, 'assets', answer.content);
            const url = `http://view.officeapps.live.com/op/view.aspx?src=${doc}`;
            await this.playUrl(min, min.conversationalService, step, url, channel);
        }
        else if (answer.content.endsWith('.pdf')) {
            const url = urlJoin('kb', `${min.instance.botId}.gbai`, `${min.instance.botId}.gbkb`, 'assets', answer.content);
            await this.playUrl(min, min.conversationalService, step, url, channel);
        }
        else if (answer.format === '.md') {
            await min.conversationalService['playMarkdown'](min, answer.content, channel, step, min.conversationalService);
        }
        else if (answer.content.endsWith('.ogg') && process.env.AUDIO_DISABLED !== 'true') {
            await this.playAudio(min, answer, channel, step, min.conversationalService);
        }
        else {
            await min.conversationalService.sendText(min, step, answer.content);
            await min.conversationalService.sendEvent(min, step, 'stop', undefined);
        }
    }
    async importKbPackage(localPath, packageStorage, instance) {
        // Imports subjects tree into database and return it.
        const subjectFile = urlJoin(localPath, 'subjects.json');
        if (Fs.existsSync(subjectFile)) {
            await this.importSubjectFile(packageStorage.packageId, subjectFile, instance);
        }
        // Import tabular files in the tabular directory.
        await this.importKbTabularDirectory(localPath, instance, packageStorage.packageId);
        // Import remaining .md files in articles directory.
        return await this.importRemainingArticles(localPath, instance, packageStorage.packageId);
    }
    /**
     * Import all .md files in artcles folder that has not been referenced by tabular files.
     */
    async importRemainingArticles(localPath, instance, packageId) {
        const files = await walkPromise(urlJoin(localPath, 'articles'));
        await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(files, async (file) => {
            if (file !== null && file.name.endsWith('.md')) {
                let content = await this.getAnswerTextByMediaName(instance.instanceId, file.name);
                if (content === null) {
                    const fullFilename = urlJoin(file.root, file.name);
                    content = Fs.readFileSync(fullFilename, 'utf-8');
                    await models_1.GuaribasAnswer.create({
                        instanceId: instance.instanceId,
                        content: content,
                        format: '.md',
                        media: file.name,
                        packageId: packageId,
                        prevId: 0 // TODO: Calculate total rows and increment.
                    });
                }
            }
        });
    }
    async importKbTabularDirectory(localPath, instance, packageId) {
        const files = await walkPromise(localPath);
        await pragmatismo_io_framework_1.CollectionUtil.asyncForEach(files, async (file) => {
            if (file !== null && file.name.endsWith('.xlsx')) {
                return await this.importKbTabularFile(urlJoin(file.root, file.name), instance.instanceId, packageId);
            }
        });
    }
    async importSubjectFile(packageId, filename, instance) {
        const subjectsLoaded = JSON.parse(Fs.readFileSync(filename, 'utf8'));
        const doIt = async (subjects, parentSubjectId) => {
            return asyncPromise.eachSeries(subjects, async (item) => {
                const value = await models_1.GuaribasSubject.create({
                    internalId: item.id,
                    parentSubjectId: parentSubjectId,
                    instanceId: instance.instanceId,
                    from: item.from,
                    to: item.to,
                    title: item.title,
                    description: item.description,
                    packageId: packageId
                });
                if (item.children) {
                    return doIt(item.children, value.subjectId);
                }
                else {
                    return item;
                }
            });
        };
        return doIt(subjectsLoaded.children, undefined);
    }
    async undeployKbFromStorage(instance, deployer, packageId) {
        await models_1.GuaribasQuestion.destroy({
            where: { instanceId: instance.instanceId, packageId: packageId }
        });
        await models_1.GuaribasAnswer.destroy({
            where: { instanceId: instance.instanceId, packageId: packageId }
        });
        await models_1.GuaribasSubject.destroy({
            where: { instanceId: instance.instanceId, packageId: packageId }
        });
        await this.undeployPackageFromStorage(instance, packageId);
    }
    /**
     * Deploys a knowledge base to the storage using the .gbkb format.
     *
     * @param localPath Path to the .gbkb folder.
     */
    async deployKb(core, deployer, localPath, min) {
        const packageName = Path.basename(localPath);
        botlib_1.GBLog.info(`[GBDeployer] Opening package: ${localPath}`);
        const instance = await core.loadInstanceByBotId(min.botId);
        botlib_1.GBLog.info(`[GBDeployer] Importing: ${localPath}`);
        const p = await deployer.deployPackageToStorage(instance.instanceId, packageName);
        await this.importKbPackage(localPath, p, instance);
        deployer.mountGBKBAssets(packageName, min.botId, localPath);
        await deployer.rebuildIndex(instance, new AzureDeployerService_1.AzureDeployerService(deployer).getKBSearchSchema(instance.searchIndex));
        botlib_1.GBLog.info(`[GBDeployer] Finished import of ${localPath}`);
    }
    async playAudio(min, answer, channel, step, conversationalService) {
        conversationalService.sendAudio(min, step, answer.content);
    }
    async playUrl(min, conversationalService, step, url, channel) {
        if (channel === 'whatsapp') {
            await min.conversationalService.sendFile(min, step, null, url, '');
        }
        else {
            await conversationalService.sendEvent(min, step, 'play', {
                playerType: 'url',
                data: url
            });
        }
    }
    async playVideo(min, conversationalService, step, answer, channel) {
        if (channel === 'whatsapp') {
            await min.conversationalService.sendFile(min, step, null, answer.content, '');
        }
        else {
            await conversationalService.sendEvent(min, step, 'play', {
                playerType: 'video',
                data: urlJoin(`${min.instance.botId}.gbai`, `${min.instance.botId}.gbkb`, 'videos', answer.content)
            });
        }
    }
    async undeployPackageFromStorage(instance, packageId) {
        await GBModel_1.GuaribasPackage.destroy({
            where: { instanceId: instance.instanceId, packageId: packageId }
        });
    }
}
exports.KBService = KBService;
//# sourceMappingURL=KBService.js.map