// (C) 2020 GoodData Corporation
import { MetadataModule } from "./metadata";
import { XhrModule } from "./xhr";
import { UserModule } from "./user";
import cloneDeepWith from "lodash/cloneDeepWith";
import isEmpty from "lodash/isEmpty";
import compact from "lodash/compact";
import omit from "lodash/omit";
import {
    IKPI,
    IAnalyticalDashboardContent,
    DashboardExport,
    IVisualizationWidget,
    IAnalyticalDashboard,
    IObjectMeta,
} from "@gooddata/typings";

/**
 * Modify how and what should be copied to the cloned dashboard
 */

export interface ICopyDashboardOptions {
    /** copy new kpi and reference it in the cloned dashboard */
    copyKpi?: boolean;
    /** copy new visualization object and reference it in the cloned widget */
    copyVisObj?: boolean;
    /** optional, default value of name is "Copy of (current dashboard title)" */
    name?: string;
    /** optional, default value of summary is (current dashboard summary) */
    summary?: string;
    /** optional, if true, the isLocked flag will be cleared for the newly created dashboard, defaults to false */
    clearLockedFlag?: boolean;
}

type UriTranslator = (oldUri: string) => string;

export function createTranslator(
    kpiMap: Map<string, string>,
    visWidgetMap: Map<string, string>,
): UriTranslator {
    return (oldUri: string): string => {
        const kpiMatch = kpiMap.get(oldUri);
        const visWidgetMatch = visWidgetMap.get(oldUri);
        if (kpiMatch) {
            return kpiMatch;
        } else if (visWidgetMatch) {
            return visWidgetMatch;
        } else {
            return oldUri;
        }
    };
}

/**
 * Updates content of the dashboard
 *
 * @param {string} dashboardUri uri of dashboard
 * @param {UriTranslator} uriTranslator gets updated widgets and kpis uri
 * @param {string} filterContext updated filter context uri
 * @experimental
 */
export function updateContent(
    analyticalDashboard: any,
    uriTranslator: UriTranslator,
    filterContext: string,
): IAnalyticalDashboardContent {
    return cloneDeepWith(
        {
            ...analyticalDashboard.content,
            filterContext,
            widgets: analyticalDashboard.content.widgets.map((uri: string) => {
                return uriTranslator(uri);
            }),
        },
        value => {
            const uri = value.uri;
            if (!uri) {
                return;
            }
            return {
                ...value,
                uri: uriTranslator(uri),
            };
        },
    );
}

export class MetadataModuleExt {
    private metadataModule: MetadataModule;
    private userModule: UserModule;
    private xhr: XhrModule;

    constructor(xhr: XhrModule) {
        this.xhr = xhr;
        this.metadataModule = new MetadataModule(xhr);
        this.userModule = new UserModule(xhr);
    }

    /**
     * @param {string} projectId id of the project
     * @param {string} dashboardUri uri of the dashboard
     * @param {ICopyDashboardOptions} options object with options:
     *          - default {} dashboard is cloned with new kpi reference and visualization widget is cloned with new
     *              visualization object reference
     *          - copyKpi {boolean} choose whether dashboard is cloned with new Kpi reference
     *          - copyVisObj {boolean} choose whether visualization widget is cloned with new visualization object reference
     *          - name {string} optional - choose name, default value is "Copy of (old title of the dashboard)"
     * @returns {string} uri of cloned dashboard
     * @experimental
     */

    public async saveDashboardAs(
        projectId: string,
        dashboardUri: string,
        options: ICopyDashboardOptions,
    ): Promise<string> {
        const objectsFromDashboard = await this.getObjectsFromDashboard(projectId, dashboardUri);
        const dashboardDetails = await this.metadataModule.getObjectDetails(dashboardUri);
        const { analyticalDashboard }: { analyticalDashboard: IAnalyticalDashboard } = dashboardDetails;
        const allCreatedObjUris: string[] = [];
        const visWidgetUris: string[] = [];
        try {
            const filterContext = await this.duplicateFilterContext(projectId, objectsFromDashboard, options);
            allCreatedObjUris.push(filterContext);
            const kpiMap = await this.duplicateOrKeepKpis(projectId, objectsFromDashboard, options);
            if (this.shouldCopyKpi(options)) {
                allCreatedObjUris.push(...Array.from(kpiMap.values()));
            }
            const visWidgetMap = await this.duplicateWidgets(projectId, objectsFromDashboard, options);
            visWidgetUris.push(...Array.from(visWidgetMap.values()));
            const translator = createTranslator(kpiMap, visWidgetMap);
            const updatedContent = updateContent(analyticalDashboard, translator, filterContext);
            const dashboardTitle = this.getDashboardName(analyticalDashboard.meta.title, options.name);
            const dashboardSummary = this.getDashboardSummary(
                analyticalDashboard.meta.summary,
                options.summary,
            );
            const duplicateDashboard = {
                ...dashboardDetails,
                analyticalDashboard: {
                    ...dashboardDetails.analyticalDashboard,
                    content: this.getDashboardDetailObject(updatedContent, filterContext),
                    meta: {
                        ...this.getSanitizedMeta(dashboardDetails.analyticalDashboard.meta, options),
                        title: dashboardTitle,
                        summary: dashboardSummary,
                    },
                },
            };

            const duplicateDashboardUri: string = (
                await this.metadataModule.createObject(projectId, duplicateDashboard)
            ).analyticalDashboard.meta.uri;

            return duplicateDashboardUri;
        } catch (err) {
            if (this.shouldCopyVisObj(options)) {
                await Promise.all(visWidgetUris.map(uri => this.cascadingDelete(projectId, uri)));
            } else {
                await Promise.all(visWidgetUris.map(uri => this.metadataModule.deleteObject(uri)));
            }
            await Promise.all(allCreatedObjUris.map(uri => this.cascadingDelete(projectId, uri)));
            return dashboardUri;
        }
    }

    /**
     * Deletes dashboard and its objects
     * (only the author of the dashboard can delete the dashboard and its objects)
     *
     * @method deleteAllObjects
     * @param {string} projectId Project identifier
     * @param {string} dashboardUri Uri of a dashboard to be deleted
     * @experimental
     */

    public async cascadingDelete(projectID: string, dashboardUri: string): Promise<any> {
        const objects: any[] = await this.metadataModule.getObjectUsing(projectID, dashboardUri);
        const currentUser: string = (await this.userModule.getAccountInfo()).profileUri;

        const objectsToBeDeleted = objects
            .filter((object: any) => object.author === currentUser)
            .map((object: any) => {
                return object.link;
            });

        return this.xhr.post(`/gdc/md/${projectID}/objects/delete`, {
            body: {
                delete: {
                    items: [dashboardUri].concat(objectsToBeDeleted),
                    mode: "cascade",
                },
            },
        });
    }

    private getDashboardDetailObject(
        updatedContent: IAnalyticalDashboardContent,
        filterContext: string,
    ): IAnalyticalDashboardContent {
        const { layout } = updatedContent;
        return {
            ...updatedContent,
            filterContext,
            widgets: [...updatedContent.widgets],
            ...(isEmpty(layout) ? {} : { layout }),
        };
    }

    private getDashboardName(originalName: string, newName?: string): string {
        if (newName !== undefined) {
            return newName;
        }
        return `Copy of ${originalName}`;
    }

    private getDashboardSummary(originalSummary?: string, newSummary?: string): string {
        if (newSummary !== undefined) {
            return newSummary;
        } else if (originalSummary !== undefined) {
            return originalSummary;
        }
        return "";
    }

    private async duplicateOrKeepKpis(
        projectId: string,
        objsFromDashboard: any[],
        options: ICopyDashboardOptions,
    ): Promise<Map<string, string>> {
        const uriMap: Map<string, string> = new Map();
        if (this.shouldCopyKpi(options)) {
            await Promise.all(
                objsFromDashboard
                    .filter((obj: any) => this.unwrapObj(obj).meta.category === "kpi")
                    .map(async (kpiWidget: any) => {
                        const { kpi }: { kpi: IKPI } = kpiWidget;
                        const toSave = {
                            kpi: {
                                meta: this.getSanitizedMeta(kpi.meta as IObjectMeta, options),
                                content: { ...kpi.content },
                            },
                        };
                        const newUriKpiObj: string = (
                            await this.metadataModule.createObject(projectId, toSave)
                        ).kpi.meta.uri;
                        uriMap.set(kpi.meta.uri as string, newUriKpiObj);
                    }),
            );
        }

        return uriMap;
    }

    private async duplicateWidgets(
        projectId: string,
        objsFromDashboard: any[],
        options: ICopyDashboardOptions,
    ): Promise<Map<string, string>> {
        const uriMap: Map<string, string> = new Map();

        await Promise.all(
            objsFromDashboard
                .filter((obj: any) => this.unwrapObj(obj).meta.category === "visualizationWidget")
                .map(async (visWidget: any) => {
                    return this.createAndUpdateWidgets(projectId, visWidget, options, uriMap);
                }),
        );

        return uriMap;
    }

    private async createAndUpdateWidgets(
        projectId: string,
        visWidget: any,
        options: ICopyDashboardOptions,
        uriMap: Map<string, string>,
    ): Promise<void> {
        const { visualizationWidget } = visWidget;
        if (this.shouldCopyVisObj(options)) {
            const visObj = await this.metadataModule.getObjectDetails(
                visualizationWidget.content.visualization,
            );
            const toSave = {
                visualizationObject: {
                    meta: this.getSanitizedMeta(visObj.visualizationObject.meta, options),
                    content: { ...visObj.visualizationObject.content },
                },
            };
            const newUriVisObj = (await this.metadataModule.createObject(projectId, toSave))
                .visualizationObject.meta.uri;

            const updatedVisWidget = {
                ...visWidget,
                visualizationWidget: {
                    meta: this.getSanitizedMeta(visWidget.visualizationWidget.meta, options),
                    content: {
                        ...visWidget.visualizationWidget.content,
                        visualization: newUriVisObj,
                    },
                },
            };
            const visUri = (await this.metadataModule.createObject(projectId, updatedVisWidget))
                .visualizationWidget.meta.uri;
            uriMap.set(visualizationWidget.meta.uri, visUri);
        } else {
            const updatedVisWidget = {
                ...visWidget,
                visualizationWidget: {
                    meta: this.getSanitizedMeta(visWidget.visualizationWidget.meta, options),
                    content: { ...visWidget.visualizationWidget.content },
                },
            };
            const { visualizationWidget } = await this.metadataModule.createObject(
                projectId,
                updatedVisWidget,
            );
            uriMap.set(visWidget.visualizationWidget.meta.uri, visualizationWidget.meta.uri);
        }
    }

    private async duplicateFilterContext(
        projectId: string,
        objsFromDashboard: any,
        options: ICopyDashboardOptions,
    ): Promise<string> {
        const originalFilterContext = objsFromDashboard.filter(
            (obj: any) => this.unwrapObj(obj).meta.category === "filterContext",
        )[0];

        const toSave = {
            filterContext: {
                meta: this.getSanitizedMeta(originalFilterContext.filterContext.meta, options),
                content: { ...originalFilterContext.filterContext.content },
            },
        };

        const { filterContext } = await this.metadataModule.createObject(projectId, toSave);
        return filterContext.meta.uri;
    }

    private getSanitizedMeta(originalMeta: IObjectMeta, options: ICopyDashboardOptions): IObjectMeta {
        return omit(
            originalMeta,
            compact([
                "identifier",
                "uri",
                "author",
                "created",
                "updated",
                "contributor",
                options && options.clearLockedFlag && "locked",
            ]),
        ) as IObjectMeta;
    }

    private async getObjectsFromDashboard(
        projectId: string,
        dashboardUri: string,
    ): Promise<Array<IKPI | DashboardExport.IFilterContext | IVisualizationWidget>> {
        const uris = await this.getObjectsUrisInDashboard(projectId, dashboardUri);
        return this.metadataModule.getObjects(projectId, uris);
    }

    private async getObjectsUrisInDashboard(projectId: string, dashboardUri: string): Promise<string[]> {
        return (
            await this.metadataModule.getObjectUsing(projectId, dashboardUri, {
                types: ["kpi", "visualizationWidget", "filterContext"],
            })
        ).map((obj: any) => {
            return obj.link;
        });
    }

    private unwrapObj(obj: any): any {
        return obj[Object.keys(obj)[0]];
    }

    private shouldCopyVisObj(options: ICopyDashboardOptions): boolean {
        return !!(options.copyVisObj || typeof options.copyVisObj === "undefined");
    }

    private shouldCopyKpi(options: ICopyDashboardOptions): boolean {
        return !!(options.copyKpi || typeof options.copyKpi === "undefined");
    }
}
