

import _ from "lodash";
import {nest} from "d3-collection";
import {CORE_API} from "../../../common/services/core-api-utils";
import {initialiseData} from "../../../common";
import {refToString, sameRef, toEntityRef} from "../../../common/entity-utils";
import {invokeFunction} from "../../../common/index";
import {downloadTextFile} from "../../../common/file-utils";


import template from "./bulk-logical-flow-parser.html";


const bindings = {
    columnMappings: '<',
    sourceData: '<',

    onInitialise: '<',
    onParseComplete: '<'
};


const initialState = {
    columnDefs: [],
    columnMappings: {},
    filterCriteria: null,
    filteredData: [],
    loading: false,
    parsedData: [],
    sourceData: [],
    summary: {},

    onInitialise: (event) => console.log('default onInitialise handler for bulk-logical-flow-parser: ', event),
    onParseComplete: (event) => console.log('default onParseComplete handler for bulk-logical-flow-parser: ', event)
};


function resolveEntityRef(entitiesByIdentifier = {}, kind, identifier) {
    const search = _.chain(identifier)
        .toLower()
        .trim()
        .value();
    const app = entitiesByIdentifier[search];
    const entityRef = app ? toEntityRef(app, kind) : null;

    return {
        identifier,
        entityRef
    };
}


function mkEntityLinkColumnDef(columnHeading, entityRefField, identifierField) {
    return {
        field: entityRefField + '.name',
        displayName: columnHeading,
        cellTemplate: `
            <div class="ui-grid-cell-contents">
                <waltz-entity-link ng-if="row.entity.${entityRefField}"
                                   entity-ref="row.entity.${entityRefField}">
                </waltz-entity-link>
                <span ng-if="!row.entity.${entityRefField}"
                      class="text-danger bg-danger">
                    <strong ng-bind="row.entity.${identifierField}"></strong>
                    <small>not found</small>
                </span>
            </div>`
    };
}


function mkColumnDefs() {
    return [
        mkEntityLinkColumnDef('Source', 'source.entityRef', 'source.identifier'),
        mkEntityLinkColumnDef('Target', 'target.entityRef', 'target.identifier'),
        mkEntityLinkColumnDef('Data Type', 'dataType.entityRef', 'dataType.identifier'),
        {
            field: 'existing',
            displayName: '',
            width: '10%',
            cellTemplate: `
                <div class="ui-grid-cell-contents">
                    <span ng-if="COL_FIELD === null"
                          class="label label-success">New</span>
                    <span ng-if="COL_FIELD"
                          class="label label-warning">Exists</span>
                </div>`
        }
    ];
}


function resolveEntities(columnMappings = {}, sourceData = [], columnResolvers = {}) {
    const targetKeys = _.keys(columnMappings);
    const mappedObjects = _.map(sourceData, sourceObj => {
        const targetObj = {};

        _.forEach(targetKeys, targetColumn => {
            const sourceColumn = columnMappings[targetColumn];
            const resolver = columnResolvers[targetColumn] || _.identity;
            targetObj[targetColumn] = resolver(sourceObj[sourceColumn]);
        });

        return targetObj;
    });
    return mappedObjects;
}


function findExistingLogicalFlowsAndDecorators(serviceBroker, sourcesAndTargets) {
    return serviceBroker
        .loadViewData(
            CORE_API.LogicalFlowStore.findBySourceAndTargetEntityReferences,
            [sourcesAndTargets],
            { force: true })
        .then(flows => {
            // add decorators
            const existingFlowIds = _.map(flows.data, 'id');
            return serviceBroker
                .loadViewData(CORE_API.DataTypeDecoratorStore.findByFlowIds, [existingFlowIds], { force: true })
                .then(decorators => {
                    const decoratorsByFlowId = _.groupBy(decorators.data, 'dataFlowId');
                    const flowsWithDecorators = _.flatMap(flows.data, f => {
                        const decorators = decoratorsByFlowId[f.id];
                        return _.map(decorators, d => Object.assign({}, f, {decorator: d}));
                    });
                    return flowsWithDecorators;
                });
        })
}


function mkParseSummary(data = []) {
    const allParsedRefs = _.flatMap(data, d => [d.source, d.target, d.dataType]);
    const summary = Object.assign(
        {
            total: data.length,
            newFlows: 0,
            existingFlows: 0,
            missingEntities: 0,
            foundEntities: 0,
            circularFlows: 0,
            nonCircularFlows: data.length
        },
        _.countBy(data, r => r.existing == null ? 'newFlows' : 'existingFlows'),
        _.countBy(allParsedRefs, p => p.entityRef == null ? 'missingEntities' : 'foundEntities'),
        _.countBy(data, r => r.source.entityRef && r.target.entityRef && sameRef(r.source.entityRef, r.target.entityRef) ? 'circularFlows' : 'nonCircularFlows'));

    summary.errors = summary.missingEntities + summary.circularFlows;
    return summary;
}


function mkFilterPredicate(criteria) {
    switch (criteria) {
        case 'ERROR':
            return (r) => r.source.entityRef === null
                || r.target.entityRef === null
                || r.dataType.entityRef === null
                || r.source.entityRef && r.target.entityRef && sameRef(r.source.entityRef, r.target.entityRef);
        case 'NEW':
            return (r) => r.existing === null;
        case 'EXISTING':
            return (r) => r.existing !== null;
        default:
            return (r) => true;
    }
}


function parseErrorCount(data = []) {
    const allParsedRefs = _.flatMap(data, d => [d.source, d.target, d.dataType]);
    const circularFlowCount = _.sumBy(data, r => r.source.entityRef
                                        && r.target.entityRef
                                        && sameRef(r.source.entityRef, r.target.entityRef) ? 1 : 0);
    const missingEntityRefs =  _.sumBy(allParsedRefs, p => p.entityRef == null ? 1 : 0);

    return missingEntityRefs + circularFlowCount;
}


function controller($q, $scope, serviceBroker) {
    const vm = initialiseData(this, initialState);

    vm.columnDefs = mkColumnDefs();

    const loadIdentifierToEntityRefMap = () => {
        return serviceBroker
            .loadViewData(CORE_API.ApplicationStore.findAll)
            .then(r => _.keyBy(r.data, k => _.toLower(k.assetCode)));
    };

    const loadDataTypeMap = () => {
        return serviceBroker
            .loadViewData(CORE_API.DataTypeStore.findAll)
            .then(r => {
                const dataTypesByCode = _.keyBy(r.data, k => _.toLower(k.code));
                const dataTypesByName = _.keyBy(r.data, k => _.toLower(k.name));
                return Object.assign({}, dataTypesByCode, dataTypesByName);
            });
    };

    const isComplete = () => {
        return parseErrorCount(vm.parsedData) === 0;
    };

    const filterResults = (criteria) => {
        vm.filterCriteria = criteria;
        return _.filter(vm.parsedData, mkFilterPredicate(criteria));
    };

    const resolveFlows = async () => {
        if(vm.columnMappings && vm.sourceData) {
            const resolvedData = resolveEntities(vm.columnMappings, vm.sourceData, vm.columnResolvers);

            if(resolvedData.length === 0 || parseErrorCount(resolvedData) > 0) {
                return resolvedData;
            }

            //compare against logical flows in database
            const sourcesAndTargets = _.map(resolvedData, p => ({source: p.source.entityRef, target: p.target.entityRef}));
            const existingFlows = await findExistingLogicalFlowsAndDecorators(serviceBroker, sourcesAndTargets);

            // nesting: source -> target -> data type
            const existingFlowsBySourceByTargetByDataType = nest()
                .key(flow => refToString(flow.source))
                .key(flow => refToString(flow.target))
                .key(flow => refToString(flow.decorator.decoratorEntity))
                .object(existingFlows);

            const resolvedFlowsWithExisting = _
                .chain(resolvedData)
                .map(p => {
                    const sourceRefString = refToString(p.source.entityRef);
                    const targetRefString = refToString(p.target.entityRef);
                    const dataTypeString = refToString(p.dataType.entityRef);
                    const existingFlows = _.get(existingFlowsBySourceByTargetByDataType, `[${sourceRefString}][${targetRefString}][${dataTypeString}]`);
                    let existing = null;
                    if(existingFlows && existingFlows.length > 0) {
                        existing = existingFlows[0];
                    }
                    return Object.assign({}, p, {existing});
                })
                .sortBy([
                    o => o.source.entityRef.name,
                    o => o.target.entityRef.name,
                    o => o.dataType.entityRef.name,
                ])
                .value();
            return resolvedFlowsWithExisting;
        }
    };


    const parseFlows = () => {
        vm.loading = true;
        $q.all([
            loadIdentifierToEntityRefMap(),
            loadDataTypeMap()
        ]).then(([entityRefsByAssetCode, dataTypeMap]) => {
            vm.columnResolvers = {
                'source': (identifier) => resolveEntityRef(entityRefsByAssetCode, 'APPLICATION', identifier),
                'target': (identifier) => resolveEntityRef(entityRefsByAssetCode, 'APPLICATION', identifier),
                'dataType': (identifier) => resolveEntityRef(dataTypeMap, 'DATA_TYPE', identifier),
            };
            return resolveFlows()
                .then(flows => {
                    vm.parsedData = flows;
                    vm.filteredData = filterResults();
                    vm.summary = mkParseSummary(vm.parsedData);

                    $scope.$apply(() => vm.loading = false);

                    const event = {
                        data: vm.parsedData,
                        isComplete
                    };
                    invokeFunction(vm.onParseComplete, event);
                })
                .catch(err => console.err('error resolving flows: ', err));
        });
    };


    vm.$onInit = () => {
        const api = {
            parseFlows
        };
        invokeFunction(vm.onInitialise, api);
    };


    vm.$onChanges =(changes) => {
    };


    vm.applyFilter = (criteria) => {
        vm.filteredData = filterResults(criteria);
    };


    vm.exportParseErrors = () => {

        const header = [
            "Source",
            "Target",
            "Data Type"
        ];

        const dataRows = _
            .chain(vm.filteredData)
            .filter(mkFilterPredicate('ERROR'))
            .map(flow => {
                return [
                    _.get(flow.source, 'entityRef.name', flow.source.identifier + ' not found'),
                    _.get(flow.target, 'entityRef.name', flow.target.identifier + ' not found'),
                    _.get(flow.dataType, 'entityRef.name', flow.dataType.identifier + ' not found')
                ];
            })
            .value();

        const rows = [header]
            .concat(dataRows);

        downloadTextFile(rows, ",", `logical_flow_errors.csv`);
    };
}


controller.$inject = [
    '$q',
    '$scope',
    'ServiceBroker'
];


const component = {
    template,
    bindings,
    controller
};


export default {
    component,
    id: 'waltzBulkLogicalFlowParser'
};
