const pick = require('object.pick');
const LoopBackContext = require('loopback-context');

module.exports = function(Self) {
    Self.setup = function() {
        Self.super_.setup.call(this);
    };

    Self.observe('after save', async function(ctx) {
        const loopBackContext = LoopBackContext.getCurrentContext();
        await logInModel(ctx, loopBackContext);
    });

    Self.observe('before save', async function(ctx) {
        let options = {};
        if (ctx.options && ctx.options.transaction)
            options.transaction = ctx.options.transaction;

        let oldInstance;
        let oldInstanceFk;
        let newInstance;

        if (ctx.data) {
            oldInstanceFk = pick(ctx.currentInstance, Object.keys(ctx.data));
            newInstance = await fkToValue(ctx.data, ctx);
            oldInstance = await fkToValue(oldInstanceFk, ctx);
            if (ctx.where && !ctx.currentInstance) {
                let fields = Object.keys(ctx.data);
                ctx.oldInstances = await ctx.Model.app.models[ctx.Model.definition.name].find({where: ctx.where, fields: fields}, options);
            }
        }
        if (ctx.isNewInstance)
            newInstance = await fkToValue(ctx.instance.__data, ctx);

        ctx.hookState.oldInstance = oldInstance;
        ctx.hookState.newInstance = newInstance;
    });

    Self.observe('before delete', async function(ctx) {
        let options = {};
        if (ctx.options && ctx.options.transaction)
            options.transaction = ctx.options.transaction;

        if (ctx.where) {
            let affectedModel = ctx.Model.definition.name;
            let definition = ctx.Model.definition;
            let deletedInstances = await ctx.Model.app.models[affectedModel].find({where: ctx.where}, options);
            let relation = definition.settings.log.relation;

            if (relation) {
                let primaryKey = ctx.Model.relations[relation].keyFrom;

                let arrangedDeletedInstances = [];
                for (let i = 0; i < deletedInstances.length; i++) {
                    if (primaryKey)
                        deletedInstances[i].originFk = deletedInstances[i][primaryKey];
                    let arrangedInstance = await fkToValue(deletedInstances[i], ctx);
                    arrangedDeletedInstances[i] = arrangedInstance;
                }
                ctx.hookState.oldInstance = arrangedDeletedInstances;
            }
        }
    });

    Self.observe('after delete', async function(ctx) {
        const loopBackContext = LoopBackContext.getCurrentContext();
        if (ctx.hookState.oldInstance)
            logDeletedInstances(ctx, loopBackContext);
    });

    async function logDeletedInstances(ctx, loopBackContext) {
        let options = {};
        if (ctx.options && ctx.options.transaction)
            options.transaction = ctx.options.transaction;

        ctx.hookState.oldInstance.forEach(async instance => {
            let userFk;
            if (loopBackContext)
                userFk = loopBackContext.active.accessToken.userId;

            let definition = ctx.Model.definition;

            let changedModelValue = definition.settings.log.changedModelValue;
            let logRecord = {
                originFk: instance.originFk,
                userFk: userFk,
                action: 'delete',
                changedModel: ctx.Model.definition.name,
                changedModelId: instance.id,
                changedModelValue: instance[changedModelValue],
                oldInstance: instance,
                newInstance: {}
            };

            delete instance.originFk;

            let logModel = definition.settings.log.model;
            await ctx.Model.app.models[logModel].create(logRecord, options);
        });
    }

    async function fkToValue(instance, ctx) {
        let options = {};
        if (ctx.options && ctx.options.transaction)
            options.transaction = ctx.options.transaction;

        let cleanInstance = JSON.parse(JSON.stringify(instance));
        let result = {};
        for (let key in cleanInstance) {
            let val = cleanInstance[key];
            if (val === undefined || val === null) continue;
            for (let key1 in ctx.Model.relations) {
                let val1 = ctx.Model.relations[key1];
                if (val1.keyFrom == key && key != 'id') {
                    let recordSet = await ctx.Model.app.models[val1.modelTo.modelName].findById(val, null, options);

                    let showField = val1.modelTo && val1.modelTo.definition.settings.log && val1.modelTo.definition.settings.log.showField && recordSet && recordSet[val1.modelTo.definition.settings.log.showField];
                    if (!showField) {
                        const showFieldNames = [
                            'name',
                            'description',
                            'code'
                        ];
                        for (field of showFieldNames) {
                            if (val1.modelTo.definition.properties && val1.modelTo.definition.properties[field] && recordSet && recordSet[field]) {
                                showField = field;
                                break;
                            }
                        }
                    }

                    if (showField && recordSet && recordSet[showField]) {
                        val = recordSet[showField];
                        break;
                    }

                    val = recordSet && recordSet.id || val;
                    break;
                }
            }
            result[key] = val;
        }
        return result;
    }

    async function logInModel(ctx, loopBackContext) {
        let options = {};
        if (ctx.options && ctx.options.transaction)
            options.transaction = ctx.options.transaction;

        let definition = ctx.Model.definition;
        let primaryKey;
        for (let property in definition.properties) {
            if (definition.properties[property].id) {
                primaryKey = property;
                break;
            }
        }

        if (!primaryKey) throw new Error('Primary key not found');
        let originId;

        // RELATIONS LOG
        let changedModelId;

        if (ctx.instance && !definition.settings.log.relation) {
            originId = ctx.instance.id;
            changedModelId = ctx.instance.id;
        } else if (definition.settings.log.relation) {
            primaryKey = ctx.Model.relations[definition.settings.log.relation].keyFrom;

            if (ctx.where && ctx.where[primaryKey])
                originId = ctx.where[primaryKey];
            else if (ctx.instance) {
                originId = ctx.instance[primaryKey];
                changedModelId = ctx.instance.id;
            }
        } else {
            originId = ctx.currentInstance.id;
            changedModelId = ctx.currentInstance.id;
        }

        // Sets the changedModelValue to save and the instances changed in case its an updateAll
        let showField = definition.settings.log.showField;
        let where;
        if (showField && (!ctx.instance || !ctx.instance[showField]) && ctx.where) {
            changedModelId = [];
            where = [];
            let changedInstances = await ctx.Model.app.models[definition.name].find({where: ctx.where, fields: ['id', showField, primaryKey]}, options);
            changedInstances.forEach(element => {
                where.push(element[showField]);
                changedModelId.push(element.id);
                originId = element[primaryKey];
            });
        } else if (ctx.hookState.oldInstance)
            where = ctx.instance[showField];


        // Set oldInstance, newInstance, userFk and action
        let oldInstance = {};
        if (ctx.hookState.oldInstance)
            Object.assign(oldInstance, ctx.hookState.oldInstance);

        let newInstance = {};
        if (ctx.hookState.newInstance)
            Object.assign(newInstance, ctx.hookState.newInstance);

        let userFk;
        if (loopBackContext)
            userFk = loopBackContext.active.accessToken.userId;

        let action = setActionType(ctx);

        removeUnloggableProperties(definition, oldInstance);
        removeUnloggableProperties(definition, newInstance);

        let logRecord = {
            originFk: originId,
            userFk: userFk,
            action: action,
            changedModel: ctx.Model.definition.name,
            changedModelId: changedModelId,
            changedModelValue: where,
            oldInstance: oldInstance,
            newInstance: newInstance
        };

        let logsToSave = setLogsToSave(where, changedModelId, logRecord, ctx);
        let logModel = definition.settings.log.model;

        await ctx.Model.app.models[logModel].create(logsToSave, options);
    }

    /**
     * Removes unwanted properties
     * @param {*} definition Model definition
     * @param {*} properties Modified object properties
     */
    function removeUnloggableProperties(definition, properties) {
        const propList = Object.keys(properties);
        const propDefs = new Map();

        for (let property in definition.properties) {
            const propertyDef = definition.properties[property];

            propDefs.set(property, propertyDef);
        }

        for (let property of propList) {
            const propertyDef = propDefs.get(property);

            if (!propertyDef) return;

            if (propertyDef.log === false)
                delete properties[property];
            else if (propertyDef.logValue === false)
                properties[property] = null;
        }
    }

    // this function retuns all the instances changed in case this is an updateAll
    function setLogsToSave(changedInstances, changedInstancesIds, logRecord, ctx) {
        let promises = [];
        if (changedInstances && typeof changedInstances == 'object') {
            for (let i = 0; i < changedInstances.length; i++) {
                logRecord.changedModelId = changedInstancesIds[i];
                logRecord.changedModelValue = changedInstances[i];
                if (ctx.oldInstances)
                    logRecord.oldInstance = ctx.oldInstances[i];
                promises.push(JSON.parse(JSON.stringify(logRecord)));
            }
        } else
            return logRecord;

        return promises;
    }

    function setActionType(ctx) {
        let oldInstance = ctx.hookState.oldInstance;
        let newInstance = ctx.hookState.newInstance;

        if (oldInstance && newInstance)
            return 'update';
        else if (!oldInstance && newInstance)
            return 'insert';

        return 'delete';
    }
};