// (C) 2022 GoodData Corporation
const Logger = require('./logger');
const uuidv4 = require('uuid/v4');
const path = require('path');
const fs = require('fs');
const _ = require('lodash');
const stringify = require('json-stable-stringify');
const { retryOperation } = require('./utils');

const WORKSPACE_SEPARATOR = ' -- ';
const USER_SEPARATOR = '--';
const { tigerClientFactory, newAxios, setAxiosAuthorizationToken, JsonApiWorkspaceInTypeEnum } = require('@gooddata/api-client-tiger');

function getTigerBackendSdk(token, host, backend) {
    if (!host.startsWith('http')) {
        host = `https://${host}`; // eslint-disable-line no-param-reassign
    }
    if (backend === 'TIGER') {
        const axios = newAxios(host, {});
        const sdkTiger = tigerClientFactory(axios);

        if (typeof token !== 'string' && !token.length) {
            throw new Error('Tiger API Token is required!');
        }

        Logger.logLn('Using Tiger API Token for authentication');
        setAxiosAuthorizationToken(axios, token);
        return sdkTiger;
    }
    throw new Error(`unsupported SDK backend: ${backend}`);
}

function mergeModels(model, customModel) {
    function customizer(objValue, srcValue) {
        if (_.isArray(objValue)) {
            return objValue.concat(srcValue);
        }

        return undefined;
    }

    return _.mergeWith(model, customModel, customizer);
}

async function createTigerWorkspace(workspaceTitle, token, host, backend, parentWorkspaceId) {
    const sdkTiger = getTigerBackendSdk(token, host, backend);
    const parentRelation = parentWorkspaceId
        ? {
            relationships: {
                parent: {
                    data: {
                        id: parentWorkspaceId,
                        type: 'workspace'
                    }
                }
            }
        }
        : {};

    const workspaceResponse = await sdkTiger.entities.createEntityWorkspaces({
        jsonApiWorkspaceInDocument: {
            data: {
                attributes: {
                    name: `${workspaceTitle}`
                },
                id: uuidv4().replace(/-/g, ''),
                type: JsonApiWorkspaceInTypeEnum.WORKSPACE,
                ...parentRelation
            }
        }
    });
    return workspaceResponse.data.data.id;
}

async function createTigerWorkspaceWithPrefix(workspacePrefix = '', token, host, backend, parentWorkspaceId) {
    const workspaceTitle = `${workspacePrefix}${WORKSPACE_SEPARATOR}${new Date().toISOString()}`;
    const workspaceId = await createTigerWorkspace(workspaceTitle, token, host, backend, parentWorkspaceId);
    return workspaceId;
}

async function getDeclarativeWorkspaceModel(fixtureFile, dataSource, schema) {
    try {
        const fixtureContent = await fs.promises.readFile(fixtureFile);
        const declarativeWorkspaceModel = await JSON.parse(fixtureContent, (k, v) => {
            if (k === 'dataSourceId') {
                return dataSource;
            }
            if (k === 'path') {
                return [schema, v[1]];
            }
            return v;
        });
        return declarativeWorkspaceModel;
    } catch (e) {
        Logger.logLn(`Warning: Unable to load and parse ${fixtureFile}`);
        return null;
    }
}

async function setTigerWorkspaceLayout(workspaceId, token, host, backend, declarativeWorkspaceModel) {
    const sdkTiger = getTigerBackendSdk(token, host, backend);
    // STL-302 admit eventual consistency and change tests to retry/wait
    retryOperation(
        () => sdkTiger.declarativeLayout.putWorkspaceLayout({ workspaceId, declarativeWorkspaceModel }),
        10,
        1000
    );
}

async function getSchemaName(token, host, backend, dataSource) {
    const sdkTiger = getTigerBackendSdk(token, host, backend);
    return sdkTiger.entities.getEntityDataSources({ id: dataSource })
        .then(response => response.data.data.attributes.schema)
        .catch(() => {
            Logger.logLn('Error : Schema not found');
            return null;
        });
}

async function setTigerWorkspaceLayoutFromFixtures(
    workspaceId,
    fixtureType,
    dataSource,
    token,
    host,
    backend,
    customModelFile
) {
    const fixtureFile = path.join(__dirname, `./assets/fixtures/TigerLayout/tiger_${fixtureType}_declarative_layout.json`);
    const schema = await getSchemaName(token, host, backend, dataSource);
    if (!schema) {
        return;
    }
    const model = await getDeclarativeWorkspaceModel(fixtureFile, dataSource, schema);
    const customModel = await getDeclarativeWorkspaceModel(customModelFile, dataSource, schema);

    const mergedModel = mergeModels(model, customModel || {});
    await setTigerWorkspaceLayout(workspaceId, token, host, backend, mergedModel);
}

async function setTigerChildWorkspaceLayoutFromFixtures(
    workspaceId,
    token,
    host,
    backend,
    customFixtureFile
) {
    const fixtureFile = path.join(__dirname, customFixtureFile);
    const customodelChild = await getDeclarativeWorkspaceModel(fixtureFile);

    await setTigerWorkspaceLayout(workspaceId, token, host, backend, customodelChild);
}

async function setTigerWorkspaceLayoutFromCustomFile(
    workspaceId,
    token,
    host,
    backend,
    dataSource,
    customModelFile,
) {
    const schema = await getSchemaName(token, host, backend, dataSource);
    if (!schema) {
        return;
    }
    const customModel = await getDeclarativeWorkspaceModel(customModelFile, dataSource, schema);
    await setTigerWorkspaceLayout(workspaceId, token, host, backend, customModel);
}

async function createMdObjectsTiger(workspaceId, token, host, backend, entityType, jsonObjectDocument) {
    const sdkTiger = getTigerBackendSdk(token, host, backend);
    switch (entityType) {
        case 'METRIC':
            return sdkTiger.entities
                .createEntityMetrics({ workspaceId, jsonApiMetricInDocument: jsonObjectDocument });
            break; // eslint-disable-line no-unreachable
        case 'VISUALIZATION':
            return sdkTiger.entities
                .createEntityVisualizationObjects({
                    workspaceId,
                    jsonApiVisualizationObjectInDocument: jsonObjectDocument
                });
            break; // eslint-disable-line no-unreachable
        case 'FILTER_CONTEXT':
            return sdkTiger.entities
                .createEntityFilterContexts({ workspaceId, jsonApiFilterContextInDocument: jsonObjectDocument });
            break; // eslint-disable-line no-unreachable
        case 'DASHBOARD':
            return sdkTiger.entities
                .createEntityAnalyticalDashboards({
                    workspaceId,
                    jsonApiAnalyticalDashboardInDocument: jsonObjectDocument
                });
            break; // eslint-disable-line no-unreachable
        case 'DASHBOARD_PLUGIN':
            return sdkTiger.entities
                .createEntityDashboardPlugins({ workspaceId, jsonApiDashboardPluginInDocument: jsonObjectDocument });
            break; // eslint-disable-line no-unreachable
        default:
            throw new Error("Don't support entity type");
    }
}

async function createEntityObjects(workspaceId, token, host, backend, entityType, metadatas) {
    // Create objects sequentially
    /* eslint-disable no-restricted-syntax */
    if (metadatas === undefined || metadatas === null) {
        Logger.logLn(`Warning: Unable to create ${entityType}, metadata is undefined or null!`);
        return;
    }
    for (const mdObject of metadatas) {
        Logger.logLn(`Creating MD object ${mdObject.data.attributes.title}...`);
        try {
            /* eslint-disable no-await-in-loop */
            await createMdObjectsTiger(workspaceId, token, host, backend, entityType, mdObject);
            Logger.logLn(' done');
        } catch (e) {
            Logger.logLn(' ERR');
            throw e;
        }
    }
}

async function createMdObjects(configPath, workspaceId, token, host, backend) {
    const metadata = JSON.parse(fs.readFileSync(path.join(__dirname, configPath), 'utf8'));
    await createEntityObjects(workspaceId, token, host, backend, 'METRIC', metadata.metrics);
    await createEntityObjects(workspaceId, token, host, backend, 'VISUALIZATION', metadata.visualizationObjects);
    await createEntityObjects(workspaceId, token, host, backend, 'FILTER_CONTEXT', metadata.filterContexts);
    await createEntityObjects(workspaceId, token, host, backend, 'DASHBOARD', metadata.analyticalDashboards);
    await createEntityObjects(workspaceId, token, host, backend, 'DASHBOARD_PLUGIN', metadata.dashboardPlugins);
}

async function deleteTigerWorkspace(workspaceId, token, host, backend) {
    const sdkTiger = getTigerBackendSdk(token, host, backend);
    await sdkTiger.entities.deleteEntityWorkspaces({ id: workspaceId });
}

async function cleanupExpiredTigerWorkspaces(workspacePrefix, timeExpired, includeWS, sizeWS, token, host, backend) {
    const sdkTiger = getTigerBackendSdk(token, host, backend);
    const workspaceResponse = await sdkTiger.entities.getAllEntitiesWorkspaces({
        include: includeWS,
        sort: ['name'],
        size: sizeWS,
        page: 0
    });
    const expiredWorkspacesFilter = (workspace) => {
        const workspaceName = workspace.attributes.name;
        const createdTime = Number(Date.parse(workspaceName.split(WORKSPACE_SEPARATOR).pop()));
        const currentTime = new Date().getTime();
        return (workspaceName.startsWith(workspacePrefix) && currentTime - createdTime > timeExpired);
    };
    const workspaceIds = workspaceResponse.data.data.filter(expiredWorkspacesFilter).map(workspace => workspace.id);
    const promises = [];
    Object.values(workspaceIds).forEach((workspaceId) => {
        Logger.logLn(`Delete Expired Tiger Workspace: ${workspaceId}`);
        promises.push(sdkTiger.entities.deleteEntityWorkspaces({ id: workspaceId }));
    });
    await Promise.all(promises);
}

async function cleanupExpiredTigerUsers(userPrefix, timeExpired, sizeList, token, host, backend) {
    const sdkTiger = getTigerBackendSdk(token, host, backend);
    const userResponse = await sdkTiger.entities.getAllEntitiesUsers({
        sort: ['id'],
        size: sizeList,
        page: 0
    });
    const expiredUsersFilter = (user) => {
        const userId = user.id;
        const createdTime = userId.split(USER_SEPARATOR).pop();
        const currentTime = new Date().getTime();
        return (userId.startsWith(userPrefix) && currentTime - createdTime > timeExpired);
    };
    const userIds = userResponse.data.data.filter(expiredUsersFilter).map(user => user.id);
    const promises = [];
    Object.values(userIds).forEach((userId) => {
        Logger.logLn(`Delete Expired Tiger User: ${userId}`);
        promises.push(sdkTiger.entities.deleteEntityUsers({ id: userId }));
    });
    await Promise.all(promises);
}

function getFixture(filename) {
    if (!fs.existsSync(filename)) {
        Logger.logLn(`${filename} does not exist`);
        return null;
    }

    try {
        const contents = fs.readFileSync(filename, 'utf8');
        return JSON.parse(contents);
    } catch (e) {
        return null;
    }
}

function getDifferenceInFixtureAnalytics(fixtureData, existingFixtureData) {
    const { analytics = {} } = fixtureData;
    const keys = Object.keys(analytics);
    const resultingAnalytics = {};

    for (const key of keys) {
        const analyticField = analytics[key];
        const existingAnalyticsField = existingFixtureData.analytics[key];

        if (!existingAnalyticsField) {
            resultingAnalytics[key] = analyticField;
            // eslint-disable-next-line no-continue
            continue;
        }

        const result = analyticField.filter((value) => {
            const exists = existingAnalyticsField.find((o) => {
                return o.id === value.id;
            });
            return !exists;
        });

        resultingAnalytics[key] = result;
    }

    return {
        analytics: resultingAnalytics
    };
}

async function getWorkspaceLayout(workspaceId, token, host, backend) {
    const sdkTiger = getTigerBackendSdk(token, host, backend);
    const workspaceResponse = await sdkTiger.declarativeLayout.getWorkspaceLayout({ workspaceId });
    return workspaceResponse.data;
}

/**
 * Exports whole workspace layout to text file as stable json
 * @param {*} fixtureType
 * @param {*} workspaceId
 * @param {*} token
 * @param {*} host
 * @param {*} backend
 * @param {*} outputFile
 */
async function exportTigerFixture(fixtureType, workspaceId, token, host, backend, outputFile) {
    const workspaceResponseData = await getWorkspaceLayout(workspaceId, token, host, backend);

    // stable stringify to keep order of keys in output file
    const fixturesText = stringify(workspaceResponseData, { space: 4 });
    fs.writeFileSync(outputFile, `${fixturesText}\n`);

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

/**
 * Exports diff between workspace layout and fixture used for workspace creation
 * @param {*} fixtureType
 * @param {*} workspaceId
 * @param {*} token
 * @param {*} host
 * @param {*} backend
 * @param {*} outputFile
 */
async function exportTigerFixtureExtension(fixtureType, workspaceId, token, host, backend, outputFile) {
    const workspaceResponseData = await getWorkspaceLayout(workspaceId, token, host, backend);

    const fixtureFile = path.join(__dirname, `./assets/fixtures/TigerLayout/tiger_${fixtureType}_declarative_layout.json`);

    const saved = getFixture(fixtureFile);
    const increment = getDifferenceInFixtureAnalytics(workspaceResponseData, saved);

    const fixturesText = stringify(increment, { space: 4 }); // stable stringify to keep order of keys in output file
    fs.writeFileSync(outputFile, `${fixturesText}\n`);

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

async function createWorkspaceFromFixture(
    workspacePrefix, fixtureType, dataSource, token, host, backend, metadataExtension
) {
    const workspaceId = await createTigerWorkspaceWithPrefix(workspacePrefix, token, host, backend);

    Logger.logLn(
        `Creating fixture. Type: ${fixtureType}, datasource: ${dataSource}, additional md objects: ${metadataExtension}`,
    );
    await setTigerWorkspaceLayoutFromFixtures(
        workspaceId,
        fixtureType,
        dataSource,
        token,
        host,
        backend,
        metadataExtension,
    );
    return workspaceId;
}

async function getOrganizationLayout(token, host, backend) {
    const sdkTiger = getTigerBackendSdk(token, host, backend);
    const organizationResponse = await sdkTiger.declarativeLayout.getOrganizationLayout();
    return organizationResponse.data;
}

/**
 * Exports whole org layout to text file as stable json
 * @param {*} token
 * @param {*} host
 * @param {*} backend
 * @param {*} outputFile
 */
async function exportOrganizationLayout(token, host, backend, outputFile) {
    const organizationResponse = await getOrganizationLayout(token, host, backend);
    const responseText = stringify(organizationResponse, { space: 4 });
    fs.writeFileSync(outputFile, `${responseText}\n`);
    Logger.logLn(`Exported organization to ${outputFile}`);
}

module.exports = {
    getTigerBackendSdk,
    createTigerWorkspace,
    createTigerWorkspaceWithPrefix,
    getDeclarativeWorkspaceModel,
    setTigerWorkspaceLayout,
    setTigerWorkspaceLayoutFromFixtures,
    setTigerWorkspaceLayoutFromCustomFile,
    deleteTigerWorkspace,
    cleanupExpiredTigerWorkspaces,
    createMdObjects,
    exportTigerFixture,
    exportTigerFixtureExtension,
    createWorkspaceFromFixture,
    cleanupExpiredTigerUsers,
    getWorkspaceLayout,
    setTigerChildWorkspaceLayoutFromFixtures,
    getOrganizationLayout,
    exportOrganizationLayout
};
