const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const UserError = require('vn-loopback/util/user-error');

module.exports = function(Self) {
    Self.ParameterizedSQL = ParameterizedSQL;

    require('../methods/vn-model/getSetValues')(Self);
    require('../methods/vn-model/getEnumValues')(Self);

    Object.assign(Self, {
        setup() {
            Self.super_.setup.call(this);

            /**
             * Setting a global transaction timeout to find out if the service
             * is blocked because the connection pool is empty.
             */
            this.once('dataSourceAttached', () => {
                let orgBeginTransaction = this.beginTransaction;
                this.beginTransaction = function(options, cb) {
                    options = options || {};
                    if (!options.timeout)
                        options.timeout = 120000;
                    return orgBeginTransaction.call(this, options, cb);
                };
            });

            // Register field ACL validation
            /* this.beforeRemote('prototype.patchAttributes', ctx => this.checkUpdateAcls(ctx));
            this.beforeRemote('updateAll', ctx => this.checkUpdateAcls(ctx));
            this.beforeRemote('patchOrCreate', ctx => this.checkInsertAcls(ctx));
            this.beforeRemote('create', ctx => this.checkInsertAcls(ctx));
            this.beforeRemote('replaceById', ctx => this.checkInsertAcls(ctx));
            this.beforeRemote('replaceOrCreate', ctx => this.checkInsertAcls(ctx)); */

            this.remoteMethod('crud', {
                description: `Create, update or/and delete instances from model with a single request`,
                accessType: 'WRITE',
                accepts: [
                    {
                        arg: 'deletes',
                        description: `Identifiers of instances to delete`,
                        type: ['Integer']
                    }, {
                        arg: 'updates',
                        description: `Instances to update with it's identifier {where, data}`,
                        type: ['Object']
                    }, {
                        arg: 'creates',
                        description: `Instances to create`,
                        type: ['Object']
                    }
                ],
                returns: {
                    type: ['object'],
                    root: true
                }
            });
        },

        async crud(deletes, updates, creates) {
            let tx = await this.beginTransaction({});

            try {
                let options = {transaction: tx};

                if (deletes) {
                    let promises = [];
                    for (let id of deletes)
                        promises.push(this.destroyById(id, options));
                    await Promise.all(promises);
                }

                if (updates) {
                    let promises = [];
                    for (let update of updates)
                        promises.push(this.upsertWithWhere(update.where, update.data, options));
                    await Promise.all(promises);
                }

                let created;
                if (creates && creates.length) {
                    try {
                        created = await this.create(creates, options);
                    } catch (error) {
                        throw error[error.length - 1];
                    }
                }

                await tx.commit();

                return created;
            } catch (error) {
                await tx.rollback();
                throw error;
            }
        },

        /**
         * Wrapper for remoteMethod() but adding the context as
         * extra argument at the beginning of arguments list.
         *
         * @param {String} methodName The method name
         * @param {Object} options The method options
         */
        remoteMethodCtx(methodName, options) {
            if (options.accepts === undefined)
                options.accepts = [];
            else if (!Array.isArray(options.accepts))
                options.accepts = [options.accepts];

            options.accepts.unshift({
                arg: 'ctx',
                type: 'Object',
                http: {source: 'context'}
            });
            this.remoteMethod(methodName, options);
        },

        /**
         * Adds a validation, marking it as exportable to the browser.
         * Exportable validation functions should be synchronous and totally
         * independent from other code because they are parsed in the browser
         * using eval().
         *
         * @param {String} propertyName The property name
         * @param {Function} validatorFn The validation function
         * @param {Object} options The validation options
         */
        validateBinded(propertyName, validatorFn, options) {
            let customValidator = function(err) {
                if (!validatorFn(this[propertyName])) err();
            };
            options.isExportable = true;
            options.bindedFunction = validatorFn;
            this.validate(propertyName, customValidator, options);
        },

        /**
         * Catches database errors overriding create() and upsert() methods.
         *
         * @param {Function} replaceErrFunc - Callback
         */
        rewriteDbError(replaceErrFunc) {
            function replaceErr(err, replaceErrFunc) {
                if (Array.isArray(err)) {
                    const errors = err.filter(error => {
                        return error != undefined && error != null;
                    });
                    let errs = [];
                    for (let e of errors) {
                        if (!(e instanceof UserError))
                            errs.push(replaceErrFunc(e));
                        else errs.push(e);
                    }
                    return errs;
                }
                return replaceErrFunc(err);
            }

            function rewriteMethod(methodName) {
                const realMethod = this[methodName];
                return async(data, options, cb) => {
                    if (options instanceof Function) {
                        cb = options;
                        options = null;
                    }

                    try {
                        const result = await realMethod.call(this, data, options);

                        if (cb) cb(null, result);
                        else return result;
                    } catch (err) {
                        let myErr = replaceErr(err, replaceErrFunc);
                        if (cb) cb(myErr);
                        else
                            throw myErr;
                    }
                };
            }

            this.once('attached', () => {
                this.remove =
                this.deleteAll =
                this.destroyAll = rewriteMethod.call(this, 'remove');
                this.upsert = rewriteMethod.call(this, 'upsert');
                this.create = rewriteMethod.call(this, 'create');
            });
        },

        /*
         * Shortcut to VnMySQL.executeP()
         */
        rawSql(query, params, options, cb) {
            return this.dataSource.connector.executeP(query, params, options, cb);
        },

        /*
         * Shortcut to VnMySQL.executeStmt()
         */
        rawStmt(stmt, options) {
            return this.dataSource.connector.executeStmt(stmt, options);
        },

        /*
         * Shortcut to VnMySQL.makeLimit()
         */
        makeLimit(filter) {
            return this.dataSource.connector.makeLimit(filter);
        },

        /*
         * Shortcut to VnMySQL.makeSuffix()
         */
        makeSuffix(filter) {
            return this.dataSource.connector.makeSuffix(filter);
        },

        /*
         * Shortcut to VnMySQL.buildModelSuffix()
         */
        buildSuffix(filter, tableAlias) {
            return this.dataSource.connector.buildModelSuffix(this.modelName, filter, tableAlias);
        },

        async checkAcls(ctx, actionType) {
            let userId = ctx.req.accessToken.userId;
            let models = this.app.models;
            let userRoles = await models.Account.getRoles(userId);
            let data = ctx.args.data;
            let modelAcls;

            function modifiedProperties(data) {
                let properties = [];

                for (property in data)
                    properties.push(property);

                return properties;
            }

            modelAcls = await models.FieldAcl.find({
                where: {
                    and: [
                        {model: this.modelName},
                        {role: {inq: userRoles}},
                        {property: '*'},
                        {or: [{actionType: '*'}, {actionType: actionType}]}
                    ]
                }
            });

            let allowedAll = modelAcls.find(acl => {
                return acl.property == '*';
            });

            if (allowedAll)
                return;

            modelAcls = await models.FieldAcl.find({
                where: {
                    and: [
                        {model: this.modelName},
                        {role: {inq: userRoles}},
                        {property: {inq: modifiedProperties(data)}},
                        {or: [{actionType: '*'}, {actionType: actionType}]}
                    ]
                }
            });

            let propsHash = {};
            for (let acl of modelAcls)
                propsHash[acl.property] = true;

            let allowedProperties = Object.keys(data).every(property => {
                return propsHash[property];
            });

            if (!allowedProperties)
                throw new UserError(`You don't have enough privileges`);
        },

        checkUpdateAcls(ctx) {
            return this.checkAcls(ctx, 'update');
        },

        checkInsertAcls(ctx) {
            return this.checkAcls(ctx, 'insert');
        }
    });
};