// (C) 2007-2020 GoodData Corporation
import md5 from "md5";
import invariant from "invariant";
import cloneDeep from "lodash/cloneDeep";
import compact from "lodash/compact";
import filter from "lodash/filter";
import first from "lodash/first";
import find from "lodash/find";
import map from "lodash/map";
import merge from "lodash/merge";
import every from "lodash/every";
import get from "lodash/get";
import isEmpty from "lodash/isEmpty";
import negate from "lodash/negate";
import partial from "lodash/partial";
import flatten from "lodash/flatten";
import set from "lodash/set";

import { Rules } from "../utils/rules";
import { sortDefinitions } from "../utils/definitions";
import { getMissingUrisInAttributesMap } from "../utils/attributesMapLoader";
import {
    getAttributes,
    getAttributesDisplayForms,
    getDefinition,
    getMeasureFilters,
    getMeasures,
    isAttributeMeasureFilter,
} from "../utils/visualizationObjectHelper";
import { IMeasure } from "../interfaces";
import { XhrModule } from "../xhr";

const notEmpty = negate(isEmpty);

function findHeaderForMappingFn(mapping: any, header: any) {
    return (
        (mapping.element === header.id || mapping.element === header.uri) && header.measureIndex === undefined
    );
}

function wrapMeasureIndexesFromMappings(metricMappings: any[], headers: any[]) {
    if (metricMappings) {
        metricMappings.forEach(mapping => {
            const header = find(headers, partial(findHeaderForMappingFn, mapping));
            if (header) {
                header.measureIndex = mapping.measureIndex;
                header.isPoP = mapping.isPoP;
            }
        });
    }
    return headers;
}

const emptyResult = {
    extendedTabularDataResult: {
        values: [],
        warnings: [],
    },
};

const MAX_TITLE_LENGTH = 1000;

function getMetricTitle(suffix: string, title: string) {
    const maxLength = MAX_TITLE_LENGTH - suffix.length;
    if (title && title.length > maxLength) {
        if (title[title.length - 1] === ")") {
            return `${title.substring(0, maxLength - 2)}…)${suffix}`;
        }
        return `${title.substring(0, maxLength - 1)}…${suffix}`;
    }
    return `${title}${suffix}`;
}

const getBaseMetricTitle = partial(getMetricTitle, "");

const CONTRIBUTION_METRIC_FORMAT = "#,##0.00%";

function getPoPDefinition(measure: IMeasure) {
    return get(measure, ["definition", "popMeasureDefinition"], {});
}

function getAggregation(measure: IMeasure) {
    return get(getDefinition(measure), "aggregation", "").toLowerCase();
}

function isEmptyFilter(metricFilter: any) {
    if (get(metricFilter, "positiveAttributeFilter")) {
        return isEmpty(get(metricFilter, ["positiveAttributeFilter", "in"]));
    }
    if (get(metricFilter, "negativeAttributeFilter")) {
        return isEmpty(get(metricFilter, ["negativeAttributeFilter", "notIn"]));
    }
    if (get(metricFilter, "absoluteDateFilter")) {
        return (
            get(metricFilter, ["absoluteDateFilter", "from"]) === undefined &&
            get(metricFilter, ["absoluteDateFilter", "to"]) === undefined
        );
    }
    return (
        get(metricFilter, ["relativeDateFilter", "from"]) === undefined &&
        get(metricFilter, ["relativeDateFilter", "to"]) === undefined
    );
}

function allFiltersEmpty(item: any) {
    return every(map(getMeasureFilters(item), f => isEmptyFilter(f)));
}

function isDerived(measure: any) {
    const aggregation = getAggregation(measure);
    return aggregation !== "" || !allFiltersEmpty(measure);
}

function getAttrTypeFromMap(dfUri: string, attributesMap: any) {
    return get(get(attributesMap, [dfUri], {}), ["attribute", "content", "type"]);
}

function getAttrUriFromMap(dfUri: string, attributesMap: any) {
    return get(get(attributesMap, [dfUri], {}), ["attribute", "meta", "uri"]);
}

function isAttrFilterNegative(attributeFilter: any) {
    return get(attributeFilter, "negativeAttributeFilter") !== undefined;
}

function getAttrFilterElements(attributeFilter: any) {
    const isNegative = isAttrFilterNegative(attributeFilter);
    const pathToElements = isNegative
        ? ["negativeAttributeFilter", "notIn"]
        : ["positiveAttributeFilter", "in"];
    return get(attributeFilter, pathToElements, []);
}

function getAttrFilterExpression(measureFilter: any, attributesMap: any) {
    const isNegative = get(measureFilter, "negativeAttributeFilter", false);
    const detailPath = isNegative ? "negativeAttributeFilter" : "positiveAttributeFilter";
    const attributeUri = getAttrUriFromMap(
        get(measureFilter, [detailPath, "displayForm", "uri"]),
        attributesMap,
    );
    const elements = getAttrFilterElements(measureFilter);
    if (isEmpty(elements)) {
        return null;
    }
    const elementsForQuery = map(elements, e => `[${e}]`);
    const negative = isNegative ? "NOT " : "";

    return `[${attributeUri}] ${negative}IN (${elementsForQuery.join(",")})`;
}

function getDateFilterExpression() {
    // measure date filter was never supported
    return "";
}

function getFilterExpression(attributesMap: any, measureFilter: any) {
    if (isAttributeMeasureFilter(measureFilter)) {
        return getAttrFilterExpression(measureFilter, attributesMap);
    }
    return getDateFilterExpression();
}

function getGeneratedMetricExpression(item: any, attributesMap: any) {
    const aggregation = getAggregation(item).toUpperCase();
    const objectUri = get(getDefinition(item), "item.uri");
    const where = filter(map(getMeasureFilters(item), partial(getFilterExpression, attributesMap)), e => !!e);

    return `SELECT ${aggregation ? `${aggregation}([${objectUri}])` : `[${objectUri}]`}${
        notEmpty(where) ? ` WHERE ${where.join(" AND ")}` : ""
    }`;
}

function getPercentMetricExpression(category: any, attributesMap: any, measure: any) {
    let metricExpressionWithoutFilters = `SELECT [${get(getDefinition(measure), "item.uri")}]`;

    if (isDerived(measure)) {
        metricExpressionWithoutFilters = getGeneratedMetricExpression(
            set(cloneDeep(measure), ["definition", "measureDefinition", "filters"], []),
            attributesMap,
        );
    }

    const attributeUri = getAttrUriFromMap(get(category, "displayForm.uri"), attributesMap);
    const whereFilters = filter(
        map(getMeasureFilters(measure), partial(getFilterExpression, attributesMap)),
        e => !!e,
    );
    const whereExpression = notEmpty(whereFilters) ? ` WHERE ${whereFilters.join(" AND ")}` : "";

    // tslint:disable-next-line:max-line-length
    return `SELECT (${metricExpressionWithoutFilters}${whereExpression}) / (${metricExpressionWithoutFilters} BY ALL [${attributeUri}]${whereExpression})`;
}

function getPoPExpression(attributeUri: string, metricExpression: string) {
    return `SELECT ${metricExpression} FOR PREVIOUS ([${attributeUri}])`;
}

function getGeneratedMetricHash(title: string, format: string, expression: string) {
    return md5(`${expression}#${title}#${format}`);
}

function getMeasureType(measure: any) {
    const aggregation = getAggregation(measure);
    if (aggregation === "") {
        return "metric";
    } else if (aggregation === "count") {
        return "attribute";
    }
    return "fact";
}

function getGeneratedMetricIdentifier(
    item: any,
    aggregation: string,
    expressionCreator: (item: any, attributesMap: any) => string,
    hasher: any,
    attributesMap: any,
) {
    const [, , , prjId, , id] = get(getDefinition(item), "item.uri", "").split("/");
    const identifier = `${prjId}_${id}`;
    const hash = hasher(expressionCreator(item, attributesMap));
    const hasNoFilters = isEmpty(getMeasureFilters(item));
    const type = getMeasureType(item);

    const prefix = hasNoFilters || allFiltersEmpty(item) ? "" : "_filtered";

    return `${type}_${identifier}.generated.${hash}${prefix}_${aggregation}`;
}

function isDateAttribute(attribute: any, attributesMap = {}) {
    return getAttrTypeFromMap(get(attribute, ["displayForm", "uri"]), attributesMap) !== undefined;
}

function getMeasureSorting(measure?: any, mdObj?: any) {
    const sorting = get(mdObj, ["properties", "sortItems"], []);
    const matchedSorting = sorting.find((sortItem: any) => {
        const measureSortItem = get(sortItem, ["measureSortItem"]);
        if (measureSortItem) {
            // only one item now, we support only 2d data
            const identifier = get(measureSortItem, [
                "locators",
                0,
                "measureLocatorItem",
                "measureIdentifier",
            ]);
            return identifier === get(measure, "localIdentifier");
        }
        return false;
    });
    if (matchedSorting) {
        return get(matchedSorting, ["measureSortItem", "direction"], null);
    }
    return null;
}

function getCategorySorting(category: any, mdObj: any) {
    const sorting = get(mdObj, ["properties", "sortItems"], []);
    const matchedSorting = sorting.find((sortItem: any) => {
        const attributeSortItem = get(sortItem, ["attributeSortItem"]);
        if (attributeSortItem) {
            const identifier = get(attributeSortItem, ["attributeIdentifier"]);
            return identifier === get(category, "localIdentifier");
        }
        return false;
    });
    if (matchedSorting) {
        return get(matchedSorting, ["attributeSortItem", "direction"], null);
    }
    return null;
}

const createPureMetric = (measure: any, mdObj: any, measureIndex: number) => ({
    element: get(measure, ["definition", "measureDefinition", "item", "uri"]),
    sort: getMeasureSorting(measure, mdObj),
    meta: { measureIndex },
});

function createDerivedMetric(measure: any, mdObj: any, measureIndex: number, attributesMap: any) {
    const { format } = measure;
    const sort = getMeasureSorting(measure, mdObj);
    const title = getBaseMetricTitle(measure.title);

    const hasher = partial(getGeneratedMetricHash, title, format);
    const aggregation = getAggregation(measure);
    const element = getGeneratedMetricIdentifier(
        measure,
        aggregation.length ? aggregation : "base",
        getGeneratedMetricExpression,
        hasher,
        attributesMap,
    );
    const definition = {
        metricDefinition: {
            identifier: element,
            expression: getGeneratedMetricExpression(measure, attributesMap),
            title,
            format,
        },
    };

    return {
        element,
        definition,
        sort,
        meta: {
            measureIndex,
        },
    };
}

function createContributionMetric(measure: any, mdObj: any, measureIndex: number, attributesMap: any) {
    const attribute = first(getAttributes(mdObj));
    const getMetricExpression = partial(getPercentMetricExpression, attribute, attributesMap);
    const title = getBaseMetricTitle(get(measure, "title"));
    const hasher = partial(getGeneratedMetricHash, title, CONTRIBUTION_METRIC_FORMAT);
    const identifier = getGeneratedMetricIdentifier(
        measure,
        "percent",
        getMetricExpression,
        hasher,
        attributesMap,
    );
    return {
        element: identifier,
        definition: {
            metricDefinition: {
                identifier,
                expression: getMetricExpression(measure),
                title,
                format: CONTRIBUTION_METRIC_FORMAT,
            },
        },
        sort: getMeasureSorting(measure, mdObj),
        meta: {
            measureIndex,
        },
    };
}

function getOriginalMeasureForPoP(popMeasure: any, mdObj: any) {
    return getMeasures(mdObj).find(
        (measure: any) =>
            get(measure, "localIdentifier") === get(getPoPDefinition(popMeasure), ["measureIdentifier"]),
    );
}

function createPoPMetric(popMeasure: any, mdObj: any, measureIndex: number, attributesMap: any) {
    const title = getBaseMetricTitle(get(popMeasure, "title"));
    const format = get(popMeasure, "format");
    const hasher = partial(getGeneratedMetricHash, title, format);

    const attributeUri = get(popMeasure, "definition.popMeasureDefinition.popAttribute.uri");
    const originalMeasure = getOriginalMeasureForPoP(popMeasure, mdObj);

    const originalMeasureExpression = `[${get(getDefinition(originalMeasure), ["item", "uri"])}]`;
    let metricExpression = getPoPExpression(attributeUri, originalMeasureExpression);

    if (isDerived(originalMeasure)) {
        const generated = createDerivedMetric(originalMeasure, mdObj, measureIndex, attributesMap);
        const generatedMeasureExpression = `(${get(generated, [
            "definition",
            "metricDefinition",
            "expression",
        ])})`;
        metricExpression = getPoPExpression(attributeUri, generatedMeasureExpression);
    }

    const identifier = getGeneratedMetricIdentifier(
        originalMeasure,
        "pop",
        () => metricExpression,
        hasher,
        attributesMap,
    );

    return {
        element: identifier,
        definition: {
            metricDefinition: {
                identifier,
                expression: metricExpression,
                title,
                format,
            },
        },
        sort: getMeasureSorting(popMeasure, mdObj),
        meta: {
            measureIndex,
            isPoP: true,
        },
    };
}

function createContributionPoPMetric(popMeasure: any, mdObj: any, measureIndex: number, attributesMap: any) {
    const attributeUri = get(popMeasure, ["definition", "popMeasureDefinition", "popAttribute", "uri"]);

    const originalMeasure = getOriginalMeasureForPoP(popMeasure, mdObj);

    const generated = createContributionMetric(originalMeasure, mdObj, measureIndex, attributesMap);
    const title = getBaseMetricTitle(get(popMeasure, "title"));

    const format = CONTRIBUTION_METRIC_FORMAT;
    const hasher = partial(getGeneratedMetricHash, title, format);

    const generatedMeasureExpression = `(${get(generated, [
        "definition",
        "metricDefinition",
        "expression",
    ])})`;
    const metricExpression = getPoPExpression(attributeUri, generatedMeasureExpression);

    const identifier = getGeneratedMetricIdentifier(
        originalMeasure,
        "pop",
        () => metricExpression,
        hasher,
        attributesMap,
    );

    return {
        element: identifier,
        definition: {
            metricDefinition: {
                identifier,
                expression: metricExpression,
                title,
                format,
            },
        },
        sort: getMeasureSorting(),
        meta: {
            measureIndex,
            isPoP: true,
        },
    };
}

function categoryToElement(attributesMap: any, mdObj: any, category: any) {
    const element = getAttrUriFromMap(get(category, ["displayForm", "uri"]), attributesMap);
    return {
        element,
        sort: getCategorySorting(category, mdObj),
    };
}

function isPoP({ definition }: any) {
    return get(definition, "popMeasureDefinition") !== undefined;
}
function isContribution({ definition }: any) {
    return get(definition, ["measureDefinition", "computeRatio"]);
}
function isPoPContribution(popMeasure: any, mdObj: any) {
    if (isPoP(popMeasure)) {
        const originalMeasure = getOriginalMeasureForPoP(popMeasure, mdObj);
        return isContribution(originalMeasure);
    }
    return false;
}
function isCalculatedMeasure({ definition }: any) {
    return get(definition, ["measureDefinition", "aggregation"]) === undefined;
}

const rules = new Rules();

rules.addRule([isPoPContribution], createContributionPoPMetric);

rules.addRule([isPoP], createPoPMetric);

rules.addRule([isContribution], createContributionMetric);

rules.addRule([isDerived], createDerivedMetric);

rules.addRule([isCalculatedMeasure], createPureMetric);

function getMetricFactory(measure: any, mdObj: any) {
    const factory = rules.match(measure, mdObj);

    invariant(factory, `Unknown factory for: ${measure}`);

    return factory;
}

function getExecutionDefinitionsAndColumns(mdObj: any, options: any, attributesMap: any) {
    const measures = getMeasures(mdObj);
    let attributes = getAttributes(mdObj);

    const metrics = flatten(
        map(measures, (measure, index) =>
            getMetricFactory(measure, mdObj)(measure, mdObj, index, attributesMap),
        ),
    );
    if (options.removeDateItems) {
        attributes = filter(attributes, attribute => !isDateAttribute(attribute, attributesMap));
    }
    attributes = map(attributes, partial(categoryToElement, attributesMap, mdObj));

    const columns = compact(map([...attributes, ...metrics], "element"));
    return {
        columns,
        definitions: sortDefinitions(compact(map(metrics, "definition"))),
    };
}

/**
 * Module for execution on experimental execution resource
 *
 * @class execution
 * @module execution
 * @deprecated The module is in maintenance mode only (just the the compilation issues are being fixed when
 *      referenced utilities and interfaces are being changed) and is not being extended when AFM executor
 *      have new functionality added.
 */
export class ExperimentalExecutionsModule {
    constructor(private xhr: XhrModule, private loadAttributesMap: any) {}

    /**
     * For the given projectId it returns table structure with the given
     * elements in column headers.
     *
     * @method getData
     * @param {String} projectId - GD project identifier
     * @param {Array} columns - An array of attribute or metric identifiers.
     * @param {Object} executionConfiguration - Execution configuration - can contain for example
     *                 property "where" containing query-like filters
     *                 property "orderBy" contains array of sorted properties to order in form
     *                      [{column: 'identifier', direction: 'asc|desc'}]
     * @param {Object} settings - Supports additional settings accepted by the underlying
     *                             xhr.ajax() calls
     *
     * @return {Object} Structure with `headers` and `rawData` keys filled with values from execution.
     */
    public getData(projectId: string, columns: any[], executionConfiguration: any = {}, settings: any = {}) {
        if (process.env.NODE_ENV !== "test") {
            // tslint:disable-next-line:no-console
            console.warn(
                "ExperimentalExecutionsModule is deprecated and is no longer being maintained. " +
                    "Please migrate to the ExecuteAfmModule.",
            );
        }

        const executedReport: any = {
            isLoaded: false,
        };

        // Create request and result structures
        const request: any = {
            execution: { columns },
        };
        // enrich configuration with supported properties such as
        // where clause with query-like filters
        ["where", "orderBy", "definitions"].forEach(property => {
            if (executionConfiguration[property]) {
                request.execution[property] = executionConfiguration[property];
            }
        });

        // Execute request
        return this.xhr
            .post(`/gdc/internal/projects/${projectId}/experimental/executions`, {
                ...settings,
                body: JSON.stringify(request),
            })
            .then(r => r.getData())
            .then(response => {
                executedReport.headers = wrapMeasureIndexesFromMappings(
                    get(executionConfiguration, "metricMappings"),
                    get(response, ["executionResult", "headers"], []),
                );

                // Start polling on url returned in the executionResult for tabularData
                return this.loadExtendedDataResults(
                    response.executionResult.extendedTabularDataResult,
                    settings,
                );
            })
            .then((r: any) => {
                const { result, status } = r;

                return {
                    ...executedReport,
                    rawData: get(result, "extendedTabularDataResult.values", []),
                    warnings: get(result, "extendedTabularDataResult.warnings", []),
                    isLoaded: true,
                    isEmpty: status === 204,
                };
            });
    }

    public mdToExecutionDefinitionsAndColumns(projectId: string, mdObj: any, options = {}) {
        const allDfUris = getAttributesDisplayForms(mdObj);
        const attributesMapPromise = this.getAttributesMap(options, allDfUris, projectId);

        return attributesMapPromise.then((attributesMap: any) => {
            return getExecutionDefinitionsAndColumns(mdObj, options, attributesMap);
        });
    }

    private getAttributesMap(options: any, displayFormUris: string[], projectId: string) {
        const attributesMap = get(options, "attributesMap", {});

        const missingUris = getMissingUrisInAttributesMap(displayFormUris, attributesMap);
        return this.loadAttributesMap(projectId, missingUris).then((result: any) => {
            return {
                ...attributesMap,
                ...result,
            };
        });
    }

    private loadExtendedDataResults(uri: string, settings: any, prevResult = emptyResult) {
        return new Promise((resolve, reject) => {
            this.xhr
                .ajax(uri, settings)
                .then(r => {
                    const { response } = r;

                    if (response.status === 204) {
                        return {
                            status: response.status,
                            result: "",
                        };
                    }

                    return {
                        status: response.status,
                        result: r.getData(),
                    };
                })
                .then(({ status, result }) => {
                    const values = [
                        ...get(prevResult, "extendedTabularDataResult.values", []),
                        ...get(result, "extendedTabularDataResult.values", []),
                    ];

                    const warnings = [
                        ...get(prevResult, "extendedTabularDataResult.warnings", []),
                        ...get(result, "extendedTabularDataResult.warnings", []),
                    ];

                    const updatedResult = merge({}, prevResult, {
                        extendedTabularDataResult: {
                            values,
                            warnings,
                        },
                    });

                    const nextUri = get(result, "extendedTabularDataResult.paging.next");
                    if (nextUri) {
                        resolve(this.loadExtendedDataResults(nextUri, settings, updatedResult));
                    } else {
                        resolve({ status, result: updatedResult });
                    }
                }, reject);
        });
    }
}
