const PSLogger = require('@sisense-ps/ps-logger/index');
const FSWrapper = require('@sisense-ps/fs-wrapper/index');

const TarWrapper = require('../lib/TarWrapper');

let CONSTANTS = require('../constants/index');
const { MESSAGES, messageMap } = require('../constants/Messages');

const Utils = require('../Utils');
const Result = require('../types/Result');
const AddonSourcesHelper = require('../helpers/AddonSourcesHelper');
const ValidationHelper = require('../helpers/ValidationHelper');
const RegExpBuilderHelper = require('../helpers/RegExpBuilderHelper');
const ExtensionHelper = require('../helpers/ExtensionHelper');
const HandledError = require('../errors/HandledError');

class BaseInstaller {
    constructor() {
        FSWrapper.calculateRootPath(CONSTANTS.ROOT_FOLDER_REGEX);
        this.addonName = Utils.extractAddonName();
        PSLogger.init(MESSAGES);
        this.parsedArguments = null;
    }

    async actionHandler(prms) {
        const prepareResult = this._prepareArguments(prms);
        if (!prepareResult.isSuccess) {
            return prepareResult;
        }

        this.parsedArguments = prepareResult.data;
        const result = new Result(true, this.parsedArguments);

        this._logStartMessage();

        const isSimple = this._handleSimpleAction();
        if (isSimple) {
            return result;
        }

        const unzipResult = await this._tryUnzipSources();
        if (!unzipResult.isSuccess) {
            return unzipResult;
        }

        ExtensionHelper.tryApplyDecorators(this);

        CONSTANTS = this._tryModifyConstants(CONSTANTS);

        const inspectSourcesResult = await this._inspectSources();
        if (!inspectSourcesResult.isSuccess) {
            throw inspectSourcesResult;
        }

        const sourceMapping = this._modifySources(inspectSourcesResult.data);
        const fromToMapResult = this._creatFromToMap(sourceMapping);

        const isExistResult = this._isComponentsExist(fromToMapResult);
        if (!isExistResult.isSuccess) {
            return result;
        }

        const isDuplicated = this._handleDuplicatedAction(fromToMapResult.data);
        if (isDuplicated) {
            result.isDuplicated = isDuplicated;
            return result;
        }

        const beforeActionResult = await this._triggerSubAction('before', fromToMapResult.data);
        if (!beforeActionResult.isSuccess) {
            return beforeActionResult;
        }

        const actionResult = this._triggerAction(fromToMapResult.data);

        if (!actionResult.isSuccess) {
            return actionResult;
        }

        const afterActionResult = await this._triggerSubAction('after', fromToMapResult.data, actionResult.data);
        if (!afterActionResult.isSuccess) {
            return afterActionResult;
        }

        return result;
    }

    //#region actions


    _install(fromToMap, component, member) {
        PSLogger.log(
            messageMap.INSTALL_PATH,
            {
                "component": component,
                "member": member.name,
                "from": member.source,
                "to": member.destination
            }
        );

        return this.__install(fromToMap, component, member);
    }

    _remove(fromToMap, component, member) {
        const isInstalled = this._checkIsMemberInstalled(component, member);
        if (!isInstalled) {
            PSLogger.log(messageMap.REMOVE_SKIP, { component, member: member.name });
            return new Result();
        }

        PSLogger.log(
            messageMap.REMOVE_PATH,
            {
                "component": component,
                "member": member.name,
                "from": member.destination
            }
        );

        return this.__remove(fromToMap, component, member);
    }

    _checkIsMemberInstalled(component, member) {
        const checkResult = this._triggerAction(
            { [component]: [member] },
            { name: "check", key: "c", value: this.parsedArguments.value },
            true
        );

        return checkResult.data[0].data[0];
    }

    _update(fromToMap, component, member) {
        this._backup(fromToMap, component, member);
        this._remove(fromToMap, component, member);

        this._install(fromToMap, component, member);
        this._restore(fromToMap, component, member);

        return { isSuccess: true };
    }

    _backup(fromToMap, component, member) {
        const isInstalled = this._checkIsMemberInstalled(component, member);
        if (!isInstalled) {
            PSLogger.log(messageMap.BACKUP_SKIP, { component, member: member.name });
            return new Result();
        }

        return this.__backup(fromToMap, component, member, this._getBackupFolderName());
    }

    _restore(fromToMap, component, member) {
        return this.__restore(fromToMap, component, member, this._getBackupFolderName());
    }

    _check(fromToMap, component, member, skipMessage = false) {
        if (!skipMessage) {
            PSLogger.log(
                messageMap.CHECK_PATH,
                {
                    "component": component,
                    "member": member.name,
                    "from": member.source,
                    "to": member.destination
                }
            );
        }
        return this.__check(fromToMap, component, member, skipMessage);
    }

    _help() {
        PSLogger.log(messageMap.HELP_MESSAGE);
    }

    _version() {
        const version = FSWrapper.getRootPath()
            .split(FSWrapper.getPlatformSeparator())
            .pop()
            .match(new RegExp('\\d+.\\d+.\\d+'))[0];
        console.log(version);
    }

    //#endregion

    /**
     * Validate and parse user input command.
     * @param {String} prms user input 
     * @returns {Result}
     */
    _prepareArguments(prms) {
        const isInputExistResult = ValidationHelper.isInputValid(prms);
        if (!isInputExistResult.isSuccess) {
            return isInputExistResult;
        }

        const patternMatches = prms.match(RegExpBuilderHelper.userInputValidationPattern());

        const validationResult = ValidationHelper.isCommandValid(patternMatches);
        if (!validationResult.isSuccess) {
            return validationResult;
        }

        const parsed = Utils.parsePassedArguments(patternMatches);
        return new Result(validationResult.isSuccess, parsed, validationResult.error);
    }

    /**
     * Trigger action based on arguments.
     * Will execute common logic for particular command, mainly manipulation with sources.
     * @param {Object} fromToMap mapping of component members and source\target pathes
     * @param {Object} commandArgs arguments wich was passed to script by user
     * @param {Boolean} skipMessage skip default log message
     * @returns {Result}
     */
    _triggerAction(fromToMap, commandArgs = this.parsedArguments, skipMessage) {
        const { key } = commandArgs;
        const actionResult = [];

        Object.keys(fromToMap).forEach((component) => {
            fromToMap[component].forEach((member) => {
                const callbackName = Utils.getOperationType(key);

                try {
                    actionResult.push(this[callbackName](fromToMap, component, member, skipMessage));
                } catch (error) {
                    throw new HandledError(error);
                }
            })
        });

        return new Result(true, actionResult);
    }

    /**
     * Trigger sub action to perform additional actions for particular command.
     * @param {String} prefix sub action type: before | after
     * @param {Object} fromToMap
     * @param {Object[]} actionResult
     * @returns {Result}
     */
    async _triggerSubAction(prefix, fromToMap, actionResult) {
        const { key } = this.parsedArguments;

        const callbackName = Utils.getOperationType(key, prefix);

        if (!this[callbackName]) {
            return { isSuccess: true, data: null };
        }

        return this[callbackName](fromToMap, actionResult);
    }

    _creatFromToMap(sourceMapping) {
        const {
            PRIORITY_ORDER,
            DESTINATION_ROOT_PATH,
            DESTINATION_PATHS,
            PLUGIN_SOURCES_FOLDER } = CONSTANTS;
        const result = {};

        PRIORITY_ORDER.forEach((priorComponentKey) => {
            if (sourceMapping[priorComponentKey]) {
                sourceMapping[priorComponentKey].forEach((componentMember) => {
                    const destObj = {
                        name: componentMember,
                        source: `${FSWrapper.getRootPath()}\\${PLUGIN_SOURCES_FOLDER}\\${priorComponentKey}\\${componentMember}`,
                        destination: `${DESTINATION_ROOT_PATH}${DESTINATION_PATHS[priorComponentKey]}\\${componentMember}`
                    };

                    // Workaround to support old names of services.
                    if (priorComponentKey === "microservice") {
                        destObj.serviceName = this._tryGetServiceName(destObj.source);
                    }

                    if (result[priorComponentKey]) {
                        result[priorComponentKey].push(destObj)
                    } else {
                        result[priorComponentKey] = [destObj]
                    }
                })
            }
        })

        return new Result(true, result);
    }

    _logStartMessage() {
        PSLogger.log(messageMap[`${this.parsedArguments.name.toUpperCase()}_STARTED`]);
    }

    _getBackupFolderName() {
        const currentDate = new Date();
        const month = Utils.addLeadingZero(currentDate.getMonth() + 1);
        const day = Utils.addLeadingZero(currentDate.getDate());
        const year = Utils.addLeadingZero(currentDate.getFullYear());

        return `${CONSTANTS.BACKUP_FOLDER}${month}.${day}.${year}`;
    }

    /**
     * Inspect addon_sources for plugin components and it members.
     * @returns {Object} key - componentName, value - array of members.
     */
    async _inspectSources() {
        const result = { isSuccess: true, data: {} };

        Object.keys(CONSTANTS.DESTINATION_PATHS).forEach((component) => {
            const componentPath = `${CONSTANTS.PLUGIN_SOURCES_FOLDER}/${component}`;
            if (FSWrapper.isPathExistSync(componentPath)) {
                const membersList = FSWrapper.readDirSync(componentPath);

                result.data[component] = membersList;
            }
        })

        return result;
    }

    async _tryUnzipSources() {
        let result = { isSuccess: true };
        const { PLUGIN_SOURCES_FOLDER, IGNORE_INSTALL_FOLDERS } = CONSTANTS;
        const sourcePath = FSWrapper.combinePath([FSWrapper.getRootPath(), `${PLUGIN_SOURCES_FOLDER}.tar.gz`]);
        const targetPath = `${FSWrapper.getRootPath()}`;
        const ignorePattern = new RegExp(`${IGNORE_INSTALL_FOLDERS.join('|')}`);

        const { unpacked, unpackCount, tarballCount } = await AddonSourcesHelper.addonSourcesStatus(sourcePath, targetPath, ignorePattern);
        const isSameCount = unpackCount === tarballCount;

        if (unpacked && isSameCount) {
            return result;
        }

        if (unpacked && !isSameCount) {
            PSLogger.log(messageMap.EXTRACT_SOURCES_CORRUPTED);

            FSWrapper.rmDirSync(PLUGIN_SOURCES_FOLDER);
        }

        PSLogger.log(messageMap.EXTRACT_SOURCES, { tarballCount: tarballCount });
        result = await TarWrapper.extract(sourcePath, targetPath, undefined, ignorePattern);
        PSLogger.log(messageMap.EXTRACT_SOURCES_SUCCES);

        return result;
    }

    _modifySources(originalSources) {
        return this.__modifySources(originalSources);
    }

    _handleSimpleAction() {
        const { name } = this.parsedArguments;
        if (['help', 'version'].includes(name)) {
            this[`_${name}`]();
            return true;
        }
        return false;
    }

    /**
     * Check if plugin already installed\removed. 
     * @param {Object} fromToMap 
     * @returns {Boolean}
     */
    _handleDuplicatedAction(fromToMap) {
        const { name, value } = this.parsedArguments;
        const isRemove = name === 'remove';
        const isInstall = name === 'install';
        if (!(isRemove || isInstall)) {
            return false;
        }

        const actionResult = this._triggerAction(
            fromToMap,
            { name: "check", key: "c", value },
            true
        );

        const statusResult = [];
        const operationResult = {};

        actionResult.data.forEach((actionResult) => {
            statusResult.push(actionResult.isSuccess);
            actionResult.data.forEach((res) => {
                operationResult[res.toString()] = 1;
            });
        });

        const isInstalled = Object.keys(operationResult).length === 1 && operationResult.hasOwnProperty(true);
        const isRemoved = Object.keys(operationResult).length === 1 && operationResult.hasOwnProperty(false);
        const isPartial = Object.keys(operationResult).length === 2;

        if ((isInstall && isRemoved)
            || (isRemove && (isInstalled || isPartial))
        ) {
            return false;
        }

        const type = (isRemove && isRemoved) || (isInstall && isInstalled) ? 'FULL' : 'PARTIAL';

        PSLogger.log(messageMap[`${name.toUpperCase()}_${type}`]);

        return true;
    }

    _tryGetServiceName(sourcePath) {
        const { WIN_SERVICE_CONFIG } = CONSTANTS;

        const fileContent = FSWrapper.readFileSync(FSWrapper.combinePath([sourcePath, WIN_SERVICE_CONFIG]), null, 'utf-8');
        const testResult = new RegExp('Sisense\.[a-zA-Z]+').exec(fileContent)[0];
        return testResult || null;
    }

    _tryModifyConstants(originalConstants) {
        return originalConstants;
    }

    _isComponentsExist(fromToMapResult) {
        const result = new Result();
        if (Object.keys(fromToMapResult.data).length === 0) {
            PSLogger.log(messageMap.NO_COMPONENTS);
            result.isSuccess = false;
        }

        return result
    }
}

module.exports = BaseInstaller;
