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 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 Self.modelBuilder.models[ctx.Model.definition.name].find({where: ctx.where, fields: fields});
            }
        }
        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) {
        if (ctx.where) {
            let affectedModel = ctx.Model.definition.name;
            let definition = ctx.Model.definition;
            let deletedInstances = await Self.modelBuilder.models[affectedModel].find({where: ctx.where});
            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) {
        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: {}
            };

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

            let logModel = definition.settings.log.model;
            await Self.modelBuilder.models[logModel].create(logRecord, transaction);
        });
    }

    async function fkToValue(instance, ctx) {
        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 val1.modelTo.findById(val);
                    val = recordSet.name; // FIXME preparar todos los modelos con campo name
                    break;
                }
            }
            result[key] = val;
        }
        return result;
    }

    async function logInModel(ctx, loopBackContext) {
        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 {
                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 changedModelValue = definition.settings.log.changedModelValue;
        if (changedModelValue && (!ctx.instance || !ctx.instance[changedModelValue])) {
            var where = [];
            changedModelId = [];
            let changedInstances = await Self.modelBuilder.models[definition.name].find({where: ctx.where, fields: ['id', changedModelValue]});
            changedInstances.forEach(element => {
                where.push(element[changedModelValue]);
                changedModelId.push(element.id);
            });
        } else if (ctx.hookState.oldInstance) {
            where = ctx.instance[changedModelValue];
        }

        // 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);

        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;

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

        await Self.modelBuilder.models[logModel].create(logsToSave, transaction);
    }

    // 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';
    }
};