// (C) 2007-2018 GoodData Corporation
const getStream = require('get-stream');
const fs = require('fs');
const archiver = require('archiver');
const uuidv4 = require('uuid/v4');
const request = require('request-promise');
const mustache = require('mustache');
const path = require('path');
const tmp = require('tmp');
const stringify = require('json-stable-stringify');

const sdk = require('@gooddata/api-client-bear').default;
const Logger = require('./logger');
const { partial } = require('./utils');

const BEAR_FIXTURE_PATHS = {
    goodsales: 'GoodSales'
};


process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // in staging we trust, needed for staging

const uploadDirId = uuidv4();

function uploadArchive(config) {
    Logger.logLn('Uploading archive...');
    const remotePath = `/gdc/uploads/${uploadDirId}/upload.zip`;
    const url = `https://${config.hostname}${remotePath}`;
    const options = {
        method: 'PUT',
        url,
        headers: {
            'content-type': 'application/octet-stream'
        }
    };

    return fs.createReadStream(config.zipFilePath)
        .pipe(request(options)
            .auth(config.username, config.password))
        .then(() => {
            Logger.logLn(`Archive successfully uploaded to ${url}`);
        });
}

function prepareArchive(config) {
    return new Promise((resolve, reject) => {
        const output = fs.createWriteStream(config.zipFilePath);
        const archive = archiver('zip', {
            zlib: { level: 9 } // Compression level
        });

        archive.pipe(output);
        archive.file(path.join(config.basePath, 'upload_info.json'), { name: 'upload_info.json' });
        archive.glob('*.csv', {
            cwd: config.basePath
        });
        archive.finalize();

        output.on('close', () => {
            Logger.logLn(`Archive created at ${config.zipFilePath}`);
            resolve(archive.pointer()); // resolve with total Bytes
        });

        archive.on('warning', reject);
        archive.on('error', reject);
    });
}

function loginUser(hostname, username, password) {
    return sdk.user.login(username, password).then(() => {
        Logger.logLn(`Logged in as ${username} at ${hostname}`);
    });
}

function createProject(config) {
    Logger.logLn(`Creating project ${config.projectTitle}...`);
    return sdk.project.createProject(config.projectTitle, config.authorizationToken)
        .then((result) => {
            const projectId = result.project.links.self.split('/')[3];
            Logger.logLn(`Project ${projectId} successfully created.`);
            return projectId;
        });
}

function deleteProject(host, projectId, username, password) {
    sdk.config.setCustomDomain(host);

    const destroyProject = () => sdk.project.deleteProject(projectId)
        .then(() => {
            Logger.logLn(`Project ${projectId} successfully deleted.`);
        }, (error) => {
            Logger.logLn(`Project ${projectId} deleting error: ${error}`);
        });

    return loginUser(host, username, password).then(destroyProject);
}

function ldmManage(config, projectId) {
    Logger.logLn('Updating LDM...');
    const maql = fs.readFileSync(path.join(config.basePath, 'model.maql'), 'utf8');
    return sdk.md.ldmManage(projectId, maql)
        .then(() => {
            Logger.logLn('LDM successfully updated');
            return projectId;
        });
}

function parseIdsFromMaql(maql) {
    const regex = /{([\w.]*)}/g;
    const identifiers = [];
    let match = null;

    while ((match = regex.exec(maql)) !== null) { // eslint-disable-line no-cond-assign
        const identifier = match[1]; // 0 - fullmatch, 1 - group
        if (!identifiers.includes(identifier)) {
            identifiers.push(identifier);
        }
    }
    return identifiers;
}

function getIdentifiers(config, metadata) {
    const maql = fs.readFileSync(path.join(config.basePath, 'model.maql'), 'utf8');
    const identifiers = parseIdsFromMaql(maql);

    if (metadata.import_identifiers) {
        identifiers.push(...metadata.import_identifiers);
    }
    return identifiers;
}

function getMustacheVariables(pairs) {
    return pairs.reduce((memo, item) => (
        Object.assign(memo, {
            [item.identifier.replace(/\./g, '_')]: item.uri
        })
    ), {});
}

function mergeVariables(initialVariables, createdObjects) {
    return createdObjects.reduce((collection, item) => (
        Object.assign(collection, {
            [item.name]: item.uri
        })
    ), initialVariables);
}

function createDefinition(template, variables) {
    return JSON.parse(mustache.render(JSON.stringify(template), variables));
}

async function createMdObjects(config, projectId) {
    let metadata;
    if (config.extensionFilePath) {
        Logger.log(`extensionFilePath defined ${config.extensionFilePath}`);
        try {
            const originalMetadataJson = JSON.parse(fs.readFileSync(path.join(config.basePath, 'metadata.json'), 'utf8'));
            const metadataExtension = fs.readFileSync(
                config.extensionFilePath, 'utf8'
            );
            const metadataExtensionJson = JSON.parse(metadataExtension);
            metadata = {
                import_identifiers: [
                    ...originalMetadataJson.import_identifiers,
                    ...metadataExtensionJson.import_identifiers
                ],
                objects: [...originalMetadataJson.objects, ...metadataExtensionJson.objects]
            };
        } catch (e) {
            Logger.log(`${e}`);
            Logger.log(`${e.stack}`);
            throw e;
        }
    } else {
        metadata = JSON.parse(fs.readFileSync(path.join(config.basePath, 'metadata.json'), 'utf8'));
    }
    const identifiers = getIdentifiers(config, metadata);

    const pairs = await sdk.md.getUrisFromIdentifiers(projectId, identifiers);

    const initialVariables = getMustacheVariables(pairs);
    const createdObjects = [];

    // Create objects sequentially
    /* eslint-disable no-restricted-syntax */
    for (const obj of metadata.objects) {
        const variables = mergeVariables(initialVariables, createdObjects);
        const object = createDefinition(obj.content, variables);
        Logger.log(`Creating MD object ${obj.name}...`);

        try {
            /* eslint-disable no-await-in-loop */
            const createdObject = await sdk.md.createObject(projectId, object);
            Logger.logLn(' done', false);
            const objType = Object.keys(createdObject)[0];
            const { uri } = createdObject[objType].meta;
            // call update to force the specified identifier (it is ignored on POSTs for some reason)
            const id = uri.match(/obj\/(\d*)/)[1];
            /* eslint-disable no-await-in-loop */
            await sdk.md.updateObject(projectId, id, object);
            createdObjects.push({
                uri,
                name: obj.name
            });
        } catch (e) {
            Logger.logLn(' ERR');
            throw e;
        }
    }

    return projectId;
}

function runEtl(projectId) {
    return sdk.md.etlPull(projectId, uploadDirId)
        .then(() => {
            Logger.logLn('ETL done');
            return projectId;
        });
}

function checkConfig(config) {
    const mandatoryKeys = [
        'basePath',
        'username',
        'password',
        'hostname',
        'authorizationToken',
        'projectTitle'
    ];
    mandatoryKeys.forEach((key) => {
        if (!config[key]) {
            Logger.log(`Config variable ${key} is missing!`, false);
            process.exit(1);
        }
    });
}

function createFixture(basePath, username, password, hostname, authorizationToken, projectTitle, extensionFilePath) {
    const host = hostname
        .replace(/^https:\/\//, '')
        .replace(/\/$/, '');

    const tmpDir = tmp.dirSync({ unsafeCleanup: true });
    const normalizedBasePath = path.isAbsolute(basePath) ? basePath : path.join(__dirname, basePath);

    let normalizedExtensionFilePath;
    if (extensionFilePath) {
        normalizedExtensionFilePath = path.isAbsolute(extensionFilePath) ?
            extensionFilePath :
            path.join(__dirname, extensionFilePath);
    }

    function cleanupTempDir(projectId) {
        Logger.logLn(`Cleaning up tem directory: ${tmpDir.name}`);
        tmpDir.removeCallback();
        return projectId;
    }

    const config = {
        basePath: normalizedBasePath,
        username,
        password,
        hostname: host,
        authorizationToken,
        projectTitle,
        zipFilePath: path.join(tmpDir.name, '/upload.zip'),
        ...normalizedExtensionFilePath ? { extensionFilePath: normalizedExtensionFilePath } : {}
    };

    checkConfig(config);

    sdk.config.setCustomDomain(config.hostname);

    return loginUser(config.hostname, config.username, config.password)
        .then(partial(prepareArchive, config))
        .then(partial(uploadArchive, config))
        .then(partial(createProject, config))
        .then(partial(ldmManage, config))
        .then(partial(createMdObjects, config))
        .then(runEtl)
        .then(cleanupTempDir)
        .catch((e) => {
            if (typeof e !== 'string') {
                Logger.logLn(JSON.stringify(e));
            } else {
                Logger.logLn(e);
            }
            if (e.response && e.response.body) {
                getStream(e.response.body).then(str => Logger.logLn(str));
            }
            process.exit(1);
        });
}

function getMdObjectUris(mdObjectString) {
    return mdObjectString.match(/\/gdc\/md\/[\w\d]*\/obj\/\d*/g);
}

const getExistingIdentifierTemplate = identifier => `{{{${identifier.replace(/\./g, '_')}}}}`;

/**
 * Skip ignored objects that are copied or created by tests
 */
function isTemporaryObject(type, title, temporaryObjectType) {
    return type === temporaryObjectType && title.indexOf('IGNORE') === 0;
}

const isExistingIdentifier = (identifiers, identifier) => identifiers.indexOf(identifier) !== -1;

const findNewMdObject = (objects, identifier) =>
    objects.find(newIdentifier => newIdentifier.identifier === identifier);

function replaceUris(mdObjectString, dependencyUris, mappings, results) {
    let result = mdObjectString;
    for (const currentUri of dependencyUris) {
        const currentIdentifier = mappings.find(mapping => mapping.uri === currentUri).identifier;

        const newObject = findNewMdObject(results.newMdObjects, currentIdentifier);
        if (isExistingIdentifier(results.existingIdentifiers, currentIdentifier)) {
            result = result.replace(currentUri, getExistingIdentifierTemplate(currentIdentifier));
        } else if (newObject) {
            result = result.replace(currentUri, `{{{${newObject.generatedName}}}}`);
        } else {
            throw new Error(`Identifier ${currentIdentifier} not found`);
        }
    }
    return result;
}

const unwrapMdObject = (mdObject) => {
    const wrapperKey = Object.keys(mdObject)[0];
    return mdObject[wrapperKey];
};

// mutates the input
// deletes meta fields, links field, tree & folders from measure
function pruneMetadataObject(mdObject) {
    const unwrappedMdObject = unwrapMdObject(mdObject);

    const keptMetaKeys = ['title', 'identifier', 'summary', 'tags', 'locked', 'unlisted', 'flags'];
    const metaKeys = Object.keys(unwrappedMdObject.meta);
    for (const metaKey of metaKeys) {
        if (keptMetaKeys.indexOf(metaKey) === -1) {
            delete unwrappedMdObject.meta[metaKey];
        }
    }

    delete unwrappedMdObject.links;
    const wrapperKey = Object.keys(mdObject)[0];
    if (wrapperKey === 'metric') {
        delete unwrappedMdObject.content.tree;
        unwrappedMdObject.content.folders = [];
    }

    return mdObject;
}

// mutates result param
const addResultObject = (name, content, result) => result.resultObjects.push({ name, content });
// mutates result param
const addNewMdObject = (identifier, generatedName, result) => result.newMdObjects.push({ identifier, generatedName });
function isSkippedMdObjectType(type) {
    const skippedMdObjectTypes = [
        'attributeDisplayForm',
        'dataSet',
        'fact',
        'attribute',
        'visualizationClass'
    ];
    return skippedMdObjectTypes.indexOf(type) !== -1;
}

async function processObject(identifier, sharedFixtureIdentifiers, workspaceId, temporaryObjectType, results) {
    let mdObject = await sdk.md.getObjectByIdentifier(workspaceId, identifier);
    const mdWrapperKey = Object.keys(mdObject)[0];

    const mdObjectTitle = mdObject[mdWrapperKey].meta.title;
    new Intl.DateTimeFormat('default', { dateStyle: 'short', timeStyle: 'long' }).format(new Date());

    if (isTemporaryObject(mdWrapperKey, mdObjectTitle, temporaryObjectType)) {
        return;
    }

    if (isSkippedMdObjectType(mdWrapperKey)) {
        results.existingIdentifiers.push(identifier);
        results.usedExistingIdentifiers.add(identifier);
        return;
    }

    process.stderr.write(`Processing ${mdWrapperKey} ${mdObjectTitle} \n`);

    const sanitizedTitle = mdObjectTitle.replace(/[^a-zA-Z0-9]/g, '_');
    const generatedName = `${mdWrapperKey}_${sanitizedTitle}_${identifier}`;
    const sharedFixtureObject = sharedFixtureIdentifiers.find(oI => oI.identifier === identifier);
    if (sharedFixtureObject) {
        // dont handle objects from shared fixture
        // only put them to memory for referencing from new objects
        addNewMdObject(identifier, sharedFixtureObject.name, results);
        return;
    }
    addNewMdObject(identifier, generatedName, results);


    mdObject = pruneMetadataObject(mdObject);

    const mdObjectString = JSON.stringify(mdObject);
    const dependencyUris = getMdObjectUris(mdObjectString);

    if (dependencyUris) {
        const mappings = await sdk.md.getIdentifiersFromUris(workspaceId, dependencyUris);
        for (const mapping of mappings) {
            if (!findNewMdObject(results.newMdObjects, mapping.identifier)) {
                if (!isExistingIdentifier(results.existingIdentifiers, mapping.identifier)) {
                    await processObject(
                        mapping.identifier,
                        sharedFixtureIdentifiers,
                        workspaceId,
                        temporaryObjectType,
                        results,
                    );
                } else {
                    results.usedExistingIdentifiers.add(mapping.identifier);
                }
            }
        }

        const replaced = replaceUris(mdObjectString, dependencyUris, mappings, results);
        addResultObject(generatedName, JSON.parse(replaced), results);
    } else {
        addResultObject(generatedName, mdObject, results);
    }
}

async function getIdentifiersOfTypes(referenceWorkspaceId, mdObjectTypes) {
    const result = [];
    for (const mdObjectType of mdObjectTypes) {
        const objects = await sdk.xhr.get(`/gdc/md/${referenceWorkspaceId}/query/${mdObjectType}`);
        const identifiers = JSON.parse(objects.responseBody).query.entries.map(
            measure => measure.identifier,
        );
        result.push(...identifiers);
    }

    return result;
}

function getSharedFixtureIdentifiers(fixtureName, fixtureVersion) {
    const fixtureFile = path.join(__dirname, `./assets/fixtures/${BEAR_FIXTURE_PATHS[fixtureName]}/${fixtureVersion}/metadata.json`);
    const fixtureFileExists = fs.existsSync(fixtureFile);
    if (!fixtureFileExists) {
        Logger.logLn(`fixture file not found: ${fixtureFile}`);
        return null;
    }

    const originalFixtureMetadata = fs.readFileSync(
        fixtureFile,
    );

    let json = '';
    try {
        json = JSON.parse(originalFixtureMetadata);
    } catch (e) {
        Logger.logLn(`origin error ${originalFixtureMetadata}`);
        return null;
    }

    return json.objects.map((fixtureObject) => {
        const mdObject = fixtureObject.content;
        const unwrappedMdObject = unwrapMdObject(mdObject);
        return {
            identifier: unwrappedMdObject.meta.identifier,
            name: fixtureObject.name
        };
    });
}

async function exportBearFixture(
    fixtureType,
    fixtureVersion,
    workspaceId,
    userName,
    password,
    host,
    temporaryObjectType,
    outputFile
) {
    const sharedFixtureIdentifiers = getSharedFixtureIdentifiers(fixtureType, fixtureVersion);

    sdk.config.setCustomDomain(host);
    await loginUser(host, userName, password);

    const inputIdentifiers = await getIdentifiersOfTypes(workspaceId, [
        'metrics',
        'visualizationobjects',
        'analyticaldashboard',
        'kpialert'
    ]);

    const results = {
        resultObjects: [],
        newMdObjects: [],
        existingIdentifiers: [],
        usedExistingIdentifiers: new Set()
    };

    for (const inputIdentifier of inputIdentifiers) {
        await processObject(
            inputIdentifier,
            sharedFixtureIdentifiers,
            workspaceId,
            temporaryObjectType,
            results
        );
    }

    const resultJSON = {
        import_identifiers: Array.from(results.usedExistingIdentifiers),
        objects: results.resultObjects
    };

    fs.writeFileSync(
        outputFile,
        stringify(resultJSON, { space: 4 }), // stable stringify to keep order of keys in output file
    );

    Logger.logLn(`${fixtureType} (version ${fixtureVersion}) exported to ${outputFile}`);
}

module.exports = {
    createFixture,
    deleteProject,
    exportBearFixture,

    // for testing
    createDefinition,
    parseIdsFromMaql,
    getMustacheVariables,
    mergeVariables,
    prepareArchive
};
