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) {
        const appModels = ctx.Model.app.models;
        const definition = ctx.Model.definition;
        const options = {};

        // Check for transactions
        if (ctx.options && ctx.options.transaction)
            options.transaction = ctx.options.transaction;

        let oldInstance;
        let newInstance;

        if (ctx.data) {
            const changes = pick(ctx.currentInstance, Object.keys(ctx.data));
            newInstance = ctx.data;
            oldInstance = changes;

            if (ctx.where && !ctx.currentInstance) {
                const fields = Object.keys(ctx.data);
                const modelName = definition.name;

                ctx.oldInstances = await appModels[modelName].find({
                    where: ctx.where,
                    fields: fields
                }, options);
            }
        }

        // Get changes from created instance
        if (ctx.isNewInstance)
            newInstance = ctx.instance.__data;

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

    Self.observe('before delete', async function(ctx) {
        const appModels = ctx.Model.app.models;
        const definition = ctx.Model.definition;
        const relations = ctx.Model.relations;

        let options = {};
        if (ctx.options && ctx.options.transaction)
            options.transaction = ctx.options.transaction;

        if (ctx.where) {
            let affectedModel = definition.name;
            let deletedInstances = await appModels[affectedModel].find({
                where: ctx.where
            }, options);

            let relation = definition.settings.log.relation;

            if (relation) {
                let primaryKey = 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) {
        const appModels = ctx.Model.app.models;
        const definition = ctx.Model.definition;
        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 changedModelValue = definition.settings.log.changedModelValue;
            let logRecord = {
                originFk: instance.originFk,
                userFk: userFk,
                action: 'delete',
                changedModel: definition.name,
                changedModelId: instance.id,
                changedModelValue: instance[changedModelValue],
                oldInstance: instance,
                newInstance: {}
            };

            delete instance.originFk;

            let logModel = definition.settings.log.model;
            await appModels[logModel].create(logRecord, options);
        });
    }

    // Get log values from a foreign key
    async function fkToValue(instance, ctx) {
        const appModels = ctx.Model.app.models;
        const relations = ctx.Model.relations;
        let options = {};

        // Check for transactions
        if (ctx.options && ctx.options.transaction)
            options.transaction = ctx.options.transaction;

        const instanceCopy = JSON.parse(JSON.stringify(instance));
        const result = {};
        for (const key in instanceCopy) {
            let value = instanceCopy[key];

            if (value instanceof Object)
                continue;

            if (value === undefined) continue;

            if (value) {
                for (let relationName in relations) {
                    const relation = relations[relationName];
                    if (relation.keyFrom == key && key != 'id') {
                        const model = relation.modelTo;
                        const modelName = relation.modelTo.modelName;
                        const properties = model && model.definition.properties;
                        const settings = model && model.definition.settings;

                        const recordSet = await appModels[modelName].findById(value, null, options);

                        const hasShowField = settings.log && settings.log.showField;
                        let showField = hasShowField && recordSet
                            && recordSet[settings.log.showField];

                        if (!showField) {
                            const showFieldNames = [
                                'name',
                                'description',
                                'code',
                                'nickname'
                            ];
                            for (field of showFieldNames) {
                                const propField = properties && properties[field];
                                const recordField = recordSet && recordSet[field];

                                if (propField && recordField) {
                                    showField = field;
                                    break;
                                }
                            }
                        }

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

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

    async function logInModel(ctx, loopBackContext) {
        const appModels = ctx.Model.app.models;
        const definition = ctx.Model.definition;
        const defSettings = ctx.Model.definition.settings;
        const relations = ctx.Model.relations;

        const options = {};
        if (ctx.options && ctx.options.transaction)
            options.transaction = ctx.options.transaction;

        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 && !defSettings.log.relation) {
            originId = ctx.instance.id;
            changedModelId = ctx.instance.id;
        } else if (defSettings.log.relation) {
            primaryKey = relations[defSettings.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 = defSettings.log.showField;
        let where;
        if (showField && (!ctx.instance || !ctx.instance[showField]) && ctx.where) {
            changedModelId = [];
            where = [];
            let changedInstances = await appModels[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);

        removeUnloggable(definition, oldInstance);
        removeUnloggable(definition, newInstance);

        oldInstance = await fkToValue(oldInstance, ctx);
        newInstance = await fkToValue(newInstance, ctx);

        // Prevent log with no new changes
        const hasNewChanges = Object.keys(newInstance).length;
        if (!hasNewChanges) return;

        let logRecord = {
            originFk: originId,
            userFk: userFk,
            action: action,
            changedModel: definition.name,
            changedModelId: changedModelId, // Model property with an different data type will throw a NaN error
            changedModelValue: where,
            oldInstance: oldInstance,
            newInstance: newInstance
        };

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

        await appModels[logModel].create(logsToSave, options);
    }

    /**
     * Removes unwanted properties
     * @param {*} definition Model definition
     * @param {*} properties Modified object properties
     */
    function removeUnloggable(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';
    }
};