const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;

module.exports = function(Self) {
    Self.setup = function() {
        Self.super_.setup.call(this);
        /*
        let disableMethods = {
            create: true,
            replaceOrCreate: true,
            patchOrCreate: true,
            upsert: true,
            updateOrCreate: true,
            exists: true,
            find: true,
            findOne: true,
            findById: true,
            deleteById: true,
            replaceById: true,
            updateAttributes: false,
            createChangeStream: true,
            updateAll: true,
            upsertWithWhere: true,
            count: true
        };
        for (let method in disableMethods) {
            // this.disableRemoteMethod(method, disableMethods[method]);
        }
        */
        this.installCrudModel('crud');
    };

    Self.defineScope = function(serverFilter) {
        this.remoteMethodCtx('list', {
            accepts: [
                {
                    arg: 'filter',
                    type: 'object',
                    description: 'Filter defining where'
                }
            ],
            returns: {
                type: [this.modelName],
                root: true
            },
            http: {
                verb: 'get',
                path: '/list'
            }
        });

        this.list = function(ctx, clientFilter, cb) {
            let clientFields = (clientFilter && clientFilter.fields) ? clientFilter.fields : [];
            let serverFields = (serverFilter && serverFilter.fields) ? serverFilter.fields : [];
            let fields = clientFields.filter(itemC => {
                return serverFields.some(itemS => itemS === itemC);
            });
            let and = [];
            let order;
            let limit;
            let filter = {order: order, limit: limit};

            if (clientFilter && clientFilter.where)
                and.push(clientFilter.where);
            if (serverFilter && serverFilter.where)
                and.push(serverFilter.where);

            if (clientFilter && clientFilter.order)
                order = clientFilter.order;
            else if (serverFilter && serverFilter.order)
                order = serverFilter.order;

            if (serverFilter && serverFilter.limit)
                limit = serverFilter.limit;
            else if (clientFilter && clientFilter.limit)
                limit = clientFilter.limit;

            filter.where = (and.length > 0) && {and: and};
            filter.fields = fields;

            this.find(filter, function(err, states) {
                if (err)
                    cb(err, null);
                else
                    cb(null, states);
            });
        };
    };

    Self.remoteMethodCtx = function(methodName, args) {
        var ctx = {
            arg: 'context',
            type: 'object',
            http: function(ctx) {
                return ctx;
            }
        };
        if (args.accepts === undefined)
            args.accepts = [];
        else if (!Array.isArray(args.accepts))
            args.accepts = [args.accepts];
        args.accepts.unshift(ctx);
        this.remoteMethod(methodName, args);
    };

    Self.getConnection = function(cb) {
        this.dataSource.connector.client.getConnection(cb);
    };

    Self.connectToService = function(ctx, dataSource) {
        this.app.dataSources[dataSource].connector.remotes.auth = {
            bearer: new Buffer(ctx.req.accessToken.id).toString('base64'),
            sendImmediately: true
        };
    };

    Self.disconnectFromService = function(dataSource) {
        this.app.dataSources[dataSource].connector.remotes.auth = {
            bearer: new Buffer('').toString('base64'),
            sendImmediately: true
        };
    };

    Self.installCrudModel = function(methodName) {
        this.remoteMethod(methodName, {
            description: 'Create, update or/and delete instances from model in a single request',
            accessType: 'WRITE',
            accepts: [
                {
                    arg: 'actions',
                    type: 'Object',
                    require: true,
                    description: 'Instances to update, example: {create: [instances], update: [instances], delete: [ids]}',
                    http: {source: 'body'}
                }
            ],
            http: {
                path: `/${methodName}`,
                verb: 'POST'
            }
        });
        this[methodName] = async actions => {
            let promises = [];
            let transaction = await this.beginTransaction({});
            let options = {transaction: transaction};

            try {
                if (actions.delete && actions.delete.length) {
                    promises.push(this.destroyAll({id: {inq: actions.delete}}, options));
                }
                if (actions.create && actions.create.length) {
                    promises.push(this.create(actions.create, options));
                }
                if (actions.update) {
                    actions.update.forEach(toUpdate => {
                        promises.push(this.upsert(toUpdate, options));
                    });
                }
                await Promise.all(promises);
                await transaction.commit();
            } catch (error) {
                await transaction.rollback();
                throw Array.isArray(error) ? error[0] : error;
            }
        };
    };

    Self.rawSql = function(query, params, cb) {
        var connector = this.dataSource.connector;
        return new Promise(function(resolve, reject) {
            connector.execute(query, params, function(error, response) {
                if (cb)
                    cb(error, response);
                if (error)
                    reject(error);
                else
                    resolve(response);
            });
        });
    };

    Self.rawStmt = function(stmt) {
        return this.rawSql(stmt.sql, stmt.params);
    };

    Self.escapeName = function(name) {
        return this.dataSource.connector.escapeName(name);
    };

    Self.buildWhere = function(filter, tableAlias) {
        let connector = this.dataSource.connector;
        let wrappedConnector = Object.create(connector);
        wrappedConnector.columnEscaped = function(model, property) {
            let sql = tableAlias
                ? connector.escapeName(tableAlias) + '.'
                : '';
            return sql + connector.columnEscaped(model, property);
        };

        return wrappedConnector.buildWhere(this.modelName, filter.where);
    };

    Self.buildLimit = function(filter) {
        let sql = new ParameterizedSQL('');
        this.dataSource.connector.applyPagination(this.modelName, sql, filter);
        return sql;
    };

    Self.buildOrderBy = function(filter) {
        let order = filter.order;

        if (!order)
            return '';
        if (typeof order === 'string')
            order = [order];

        let clauses = [];

        for (let clause of order) {
            let sqlOrder = '';
            let t = clause.split(/[\s,]+/);
            let names = t[0].split('.');

            if (names.length > 1)
                sqlOrder += this.escapeName(names[0]) + '.';
            sqlOrder += this.escapeName(names[names.length - 1]);

            if (t.length > 1)
                sqlOrder += ' ' + (t[1].toUpperCase() == 'ASC' ? 'ASC' : 'DESC');

            clauses.push(sqlOrder);
        }

        return `ORDER BY ${clauses.join(', ')}`;
    };

    Self.buildPagination = function(filter) {
        return ParameterizedSQL.join([
            this.buildOrderBy(filter),
            this.buildLimit(filter)
        ]);
    };

    Self.buildSuffix = function(filter, tableAlias) {
        return ParameterizedSQL.join([
            this.buildWhere(filter, tableAlias),
            this.buildPagination(filter)
        ]);
    };

    require('../methods/vn-model/installMethod')(Self);
    require('../methods/vn-model/validateBinded')(Self);
};