const SharedConstants = require('../../shared/constants/SharedConstants');
const UtilDate = require('../utils/UtilDate');
const fs = require('fs-extra');

const FILES_DIR = '/files/';
const MODULES_DIR = '/custom-component-modules/';
const JSON_DIR = '/json/';

const APP_DIR = process.cwd();
const PACKAGE_PATH = APP_DIR + "/install-packages";

const APP_FILES_PATH = APP_DIR + FILES_DIR;
const CUSTOM_COMPONENT_PATH = APP_DIR + '/server/public/vendor' + MODULES_DIR;

const TYPE_APPLICATION_ID = 'applicationId';
const TYPE_TAG_APPLICATION_ID = 'tagApplicationId';
const TYPE_BRANCH_APPLICATION_ID = 'branchApplicationId';

// build array of file names with its subdirectory
// returns { name: '{file name}', files: [{array of files names}]}
async function getFileNames(dir) {
    let filesNames = [];

    var files = await fs.readdir(dir);
    for (var i in files) {
        let fileName = files[i];
        var name = dir + '/' + fileName;
        if ((await fs.stat(name)).isDirectory()) {
            let subFiles = await getFileNames(name);
            filesNames.push({ "name": fileName, files: subFiles });
        } else if (!(/(^|[\/\\])\../.test(fileName))) {
            filesNames.push({ "name": fileName });
        }
    }

    return filesNames;
};

// returns the package directory name
// removing .zip and 7z file extension
const getPackageFileName = file => {
   return file.originalFilename.replace(/.zip|.7z/i, '');
};

const extract7z = packageFilePath => {
    const sevenBin = require('7zip-bin');
    const { extractFull } = require('node-7z');

    return new Promise((resolve, reject) => {
        const componentExtract = extractFull(packageFilePath, PACKAGE_PATH, {
            $bin: sevenBin.path7za
        });

        componentExtract.on('end', function () {
            resolve();
        });
    });
};

const extractZip = packageFilePath => {
    const unzipper = require('unzipper');

    return new Promise((resolve, reject) => {
        fs.createReadStream(packageFilePath)
            .pipe(unzipper.Extract({ path: PACKAGE_PATH }))
            .on('finish', () => {
                resolve();
            });
    });
};

// extract files from the uploaded zip/7z
// returns file names, see getFileNames
const extractPackageFiles = async (file) => {
    let packageFilePath = PACKAGE_PATH + "/" + file.originalFilename;
    await fs.move(file.path, packageFilePath, { overwrite: true });

    if (packageFilePath.toLowerCase().endsWith(".zip")) {
        await extractZip(packageFilePath);
    } else {
        await extract7z(packageFilePath);
    }

    await fs.unlink(packageFilePath);

    let fileNameNoExtension = getPackageFileName(file);
    let extractedPath = PACKAGE_PATH + "/" + fileNameNoExtension;

    let extractedFiles = (await getFileNames(extractedPath)).filter(f =>
        ['files', 'custom-component-modules', 'json', 'application.json'].indexOf(f.name) >= 0
    );

    return extractedFiles;
};

const moveFilesTo = async (from, to) => {
    return await fs.copy(from, to, {
        overwrite: true,
        filter: n => {
            return /(^|[\/\\])\../.test(n);
        }
    });
};

const mkDirIfNotExist = async (path) => {
    try {
        await fs.access(path, fs.constants.R_OK || fs.constants.W_OK);
    } catch (e) {
        await fs.mkdir(path, { recursive: true });
    }
};

const processFiles = async (files, extractedPath) => {
    await mkDirIfNotExist(APP_FILES_PATH);

    if (!files || files.length === 0) {
        return;
    }

    for (let f of files.files) {
        let from = extractedPath + FILES_DIR + f.name;
        let to = APP_FILES_PATH + f.name;
        await moveFilesTo(from, to);
        f.status = "copied";
    }
};

const processModules = async (modules, extractedPath) => {
    await mkDirIfNotExist(CUSTOM_COMPONENT_PATH);

    if (!modules || modules.length === 0) {
        return;
    }

    for (let m of modules.files) {
        let from = extractedPath + MODULES_DIR + m.name;
        let to = CUSTOM_COMPONENT_PATH + m.name;
        if (!m.name.startsWith(".")) {
            await moveFilesTo(from, to);
            m.status = "moved";
        }
    }

    return;
};

const importDocument = async (document, userId, file) => {
    var exists = await DataService.documentExists(document.documentId);

    if (!document.systemHeader) {
        document.systemHeader = {};
    }

    try {
        let currentVersion = true;

        if (exists) {
            var existingDocument = await DataService.findOne(document.documentId, userId);
            logger.info(existingDocument);
            if (existingDocument.systemHeader.versionId === document.systemHeader.versionId) {
                // if versionId and currentVersion match - do not import
                file.status = `error: Document exists with the same version ${document.systemHeader.versionId}`;
                return;
            } else if (!document.systemHeader.versionId) {
                // if versionId does not exist, and createdDate less than any other versions
                // then import with currentVersion false.

                // if versionId does not exist, and createdDate is greater than any version
                // then import as currentVersion true
                if (!document.systemHeader.createdDate) {
                    currentVersion = false;
                } else if (!existingDocument.systemHeader.createdDate) {
                    currentVersion = true;
                } else {
                    let diff = UtilDate.compare(document.systemHeader.createdDate,
                        existingDocument.systemHeader.createdDate);

                    if (diff >= 0) {
                        currentVersion = true;
                    } else {
                        currentVersion = false;
                    }
                }
            }

            document.systemHeader.currentVersion = currentVersion;

            let options = {};
            if (!currentVersion) {
               options.importPreviousVersion = true;
               file.status = "inserted as previous version";
            } else {
                file.status = "updated";
            }

            document = await DataService.update(document.documentId, document, userId, options);
        } else {
            document = await DataService.insert(document, userId);
            file.status = "inserted";
        }
    } catch (err) {
        file.status = `error: ${err}`;
    }
};

const processJSON = async (jsonFiles, extractedPath, userId) => {
    let jsonPath = extractedPath + JSON_DIR;

    const processJSONFile = async (file, parentDir) => {
        let jsonString = await fs.readFile(jsonPath + (parentDir ? parentDir + '/': '') + file.name);

        try {
            let json = JSON.parse(jsonString);

            if (json.length) { // if array
                for (let document of json) {
                    return await importDocument(document, userId, file);
                }
            } else {
                return await importDocument(json, userId, file);
            }
        } catch (err) {
            file.status = err;
        }

    };

    for (let jsonFile of jsonFiles.files) {
        if (jsonFile.files && jsonFile.files.length) { // contains subfiles
            for (let subFile of jsonFile.files) {
                await processJSONFile(subFile, jsonFile.name);
            }
        } else {
            await processJSONFile(jsonFile);
        }
    }
};

module.exports = {
    deletePackage: async (type, documentId, userId) => {
        if (!userId) {
            throw new Error("No logged in user in call to find.");
        }

        if (!documentId) {
            throw new Error("No documentId to find.");
        }

        type = type || TYPE_APPLICATION_ID;

        let documentsQuery;

        if (type === TYPE_APPLICATION_ID) {
            documentsQuery = { "systemHeader.applicationIds": { "$in": [documentId] } };
        } else if (type === TYPE_TAG_APPLICATION_ID) {
            documentsQuery = { "systemHeader.tagApplicationId": documentId };
        } else if (type === TYPE_BRANCH_APPLICATION_ID) {
            documentsQuery = { "systemHeader.branchApplicationIds": { "$in": [documentId] } };
        }

        let documents = await DataService.findAll(documentsQuery, userId);
        let results = {};

        for (let doc of documents) {
            try {
                await DataService.destroy(doc.documentId, userId);
                results[doc.documentId] = 'deleted';
            } catch (e) {
                results[doc.documentId] = e.message;
            }
        }

        return results;
    },
    // * documentId = documentId of application document
    // * userId = documentId of logged in user
    exportPackage: async (documentId, userId) => {
        var zip = new require('adm-zip')();

        if (!userId) {
            throw new Error("No logged in user in call to find.");
        }

        if (!documentId) {
            throw new Error("No documentId to find.");
        }

        let appDoc = await DataService.findOne(documentId, userId);

        let installPath = appDoc.packagePath || documentId;

        if (appDoc.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_APPLICATION) {
            let packagePath = `./install-packages/${installPath}/json`;
            let documentsQuery = { "systemHeader.applicationIds": { "$in": [documentId] } };

            await mkDirIfNotExist(packagePath);

            if (appDoc.isTag) {
                documentsQuery = { "systemHeader.tagApplicationId": documentId };
            } else if (appDoc.parentRel && appDoc.parentRel.length > 0) {
                documentsQuery = { "systemHeader.branchApplicationIds": { "$in": [documentId] } };
            }

            let documents = await DataService.findAll(documentsQuery, userId);

            for (let doc of documents) {
                delete doc._id;
                let path = '';

                if (doc.systemHeader &&
                    doc.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_DOCUMENT) {
                    path += packagePath + '/03-document';
                } else if (doc.systemHeader &&
                    doc.systemHeader.systemType === SharedConstants.SYSTEM_TYPE_TEMPLATE) {
                    path += packagePath + '/02-template';
                } else {
                    path += packagePath + '/01-others';
                }

                await mkDirIfNotExist(path);
                await fs.writeFile(path + `/${doc.systemHeader.summaryName}-${doc.documentId}.json`,
                JSON.stringify(doc, null, 4));
            }

            zip.addLocalFolder(packagePath);
            zip.writeZip(`${packagePath}.zip`);

            return packagePath;
        } else {
            throw new Error(`${documentId} is not an application document.`);
        }
    },
    importPackage: async (file, userId) => {
        try {
            let extractedFiles = await extractPackageFiles(file);
            const getFiles = (folderName) => extractedFiles.find(file => file.name === folderName);
            let extractedPath = PACKAGE_PATH + "/" + getPackageFileName(file);


            let files = getFiles("files");
            let modules = getFiles("custom-component-modules");
            let json = getFiles("json");

            await processFiles(files, extractedPath);
            await processModules(modules, extractedPath);
            await processJSON(json, extractedPath, userId);

            try {
                let applicationFile = getFiles("application.json");

                let jsonString = await fs.readFile(extractedPath + '/' + applicationFile.name);
                let application = JSON.parse(jsonString);

                await fs.rename(extractedPath, PACKAGE_PATH + "/" + application.packageDirectory);
            } catch (err) {
                logger.error(err);
            }

            return extractedFiles;
        } catch (err) {
            logger.error("Error installing package: " + err.message) ;
            throw err;
        }
    },
    _testUtils: {  //exposed for test only
        importDocument
    }
};