'use strict';

const Controller = require('./controller');
const _ = require('lodash');
const __ = require('paradox-utils');

/**
 * @deprecated use restful-controller with new api-manager.
 */
class RestController extends Controller {

    constructor(model, options) {
        super();

        this.options = _.defaults(options || {}, {
            limit: 1000,
            filters: {},
            order: {},
            fields: [],
        });
    }

    model() {
        throw new Error('model not defined');
    }

    /* -------------------------------------------------------------------------
     * Methods
     * ---------------------------------------------------------------------- */

    fetch(options) {
        if(!options || !options.where || Object.keys(options.where).length < 1) {
            throw new Error('where required');
        }

        return this
            .model()
            .then(Model => {
                return Model
                    .findOne(options);
            });
    }

    count(options) {
        return Promise.resolve(this.options.limit);
    }

    list(options) {
        return this
            .model()
            .then(Model => {
                return Model
                    .findAll(options);
            });
    }

    create(data, options) {
        return this
            .model()
            .then(Model => {
                if(!options || !options.where || Object.keys(options.where).length < 1) {
                    return Model.create(data, options);
                }

                return Model
                    .findOrBuild(options)
                    .spread(entity => {
                        return entity.update(data);
                    });
            });
    }

    update(data, options) {
        if(!options || !options.where || Object.keys(options.where).length < 1) {
            throw new Error('where required');
        }

        return this
            .model()
            .then(Model => {
                return Model
                    .findOrBuild(options)
                    .spread(entity => {
                        return entity.update(data);
                    });
            })

    }

    destroy(options) {
        if(!options || !options.where || Object.keys(options.where).length < 1) {
            throw new Error('where required');
        }

        return this
            .model()
            .then(Model => {
                return Model
                    .destroy(options);
            });
    }

    /* -------------------------------------------------------------------------
     * Router
     * ---------------------------------------------------------------------- */

    // invoke(method, args) {
    //     return Promise.reject('forbidden direct invoke', 404);
    // }

    register(router, controller) {
        this.router = router;

        let parts = controller.split(/\./);

        let routerUrl;
        let routerUrlId;

        if(parts.length == 2) {
            let namespace = parts[0];
            controller = parts[1];

            routerUrl = '/' + namespace + '/' + controller;
            routerUrlId = '/' + namespace + '/' + controller + '/:id';

        } else {
            controller = parts[0];

            routerUrl = '/' + controller;
            routerUrlId = '/' + controller + '/:id';
        }

        router.route(routerUrl)
            .get(this.routerList.bind(this))
            .post(this.routerCreate.bind(this));

        router.route(routerUrlId)
            .get(this.routerShow.bind(this))
            .put(this.routerUpdate.bind(this))
            .patch(this.routerUpdate.bind(this))
            .delete(this.routerDestroy.bind(this));

        return this;
    }

    preload() {
        return Promise.resolve();
    }

    /* -------------------------------------------------------------------------
     * Router capture
     * ---------------------------------------------------------------------- */

    routerList(req, res) {
        let where = this.extractRequestFilters(req);
        let limit = this.extractRequestLimit(req);
        let properties = this.extractRequestFields(req);
        let offset = this.extractRequestOffset(req);
        let order = this.extractRequestOrder(req);

        let options = {
            where,
            limit,
            offset,
            // order,
            properties
        };

        this.count(options)
            .then((total) => {
                res.setHeader('X-Total-Count', total);
                res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count');

                return this
                    .list(options)
                    .then(rows => {
                        return this.applySort(rows, order);
                    });
            })
            .then(
                result => {
                    this.outputSuccess(res, result);
                },
                err => {
                    this.outputError(res, err);
                }
            );
    }

    routerCreate(req, res) {
        let data = this.extractRequestParams(req);

        return this.getPrimaryKey()
            .then(primaryKey => {
                if(primaryKey in data && !data[primaryKey]) {
                    delete data[primaryKey];
                }

                return this
                    .create(data)
                    .then(
                        result => {
                            this.outputSuccess(res, result);
                        },
                        err => {
                            this.outputError(res, err);
                        }
                    );
            });
    }

    routerShow(req, res) {
        let key = this.extractRequestId(req);

        return this.getPrimaryKey()
            .then(primaryKey => {
                let where = {};
                where[primaryKey] = key;

                let options = {
                    where: where
                };

                this
                    .fetch(options)
                    .then(
                        result => {
                            this.outputSuccess(res, result);
                        },
                        err => {
                            this.outputError(res, err);
                        }
                    );
            });
    }

    routerUpdate(req, res) {
        let key = this.extractRequestId(req);
        let data = this.extractRequestParams(req);

        return this.getPrimaryKey()
            .then(primaryKey => {
                let where = {};
                where[primaryKey] = key;

                let options = {
                    where: where
                };

                this
                    .update(data, options)
                    .then(
                        result => {
                            this.outputSuccess(res, result);
                        },
                        err => {
                            this.outputError(res, err);
                        }
                    );
            });
    }

    routerDestroy(req, res) {
        let key = this.extractRequestId(req);

        this.getPrimaryKey()
            .then(primaryKey => {
                let where = {};
                where[primaryKey] = key;

                let options = {
                    where: where
                };

                this.destroy(options)
                    .then(
                        result => {
                            this.outputSuccess(res, result);
                        },
                        err => {
                            this.outputError(res, err);
                        }
                    );
            });
    }

    /* -------------------------------------------------------------------------
     * Parse input
     * ---------------------------------------------------------------------- */

    extractRequestId(req) {
        return req.params.id;
    }

    extractRequestOrder(req) {
        let sortKey = req.query.order || 'created_at';
        let sortDir = req.query.order_dir || 'asc';

        if(sortDir === 'asc') {
            sortDir = 1;
        } else {
            sortDir = -1;
        }

        let sort = {};
        sort[sortKey] = sortDir;

        let res = [];

        for(let key in sort) {
            res.push([key, sort[key] == 1 ? 'ASC' : 'DESC'])
        }

        return res;
    }

    extractRequestFilters(req) {
        let filters = {};

        if(req.query['filters']) {
            let _filters = JSON.parse(req.query['filters']);

            if(_.isPlainObject(_filters)) {
                for(let key in _filters) {
                    filters[key] = _filters[key];
                }

            } else if(_.isArray(_filters)) {
                _filters.forEach(filter => {
                    if(!_.isArray(filter) || filter.length < 2 || filter.length > 3) {
                        throw new Error('invalid filter format');
                    }

                    if(filter.length == 2) {
                        filters[filter[0]] = filter[1];

                    } else if(filter.length == 3) {
                        let val = {};
                        let eq = this.convertOperator(filter[1]);
                        val[eq] = filter[2];

                        filters[filter[0]] = val;
                    }
                });
            }
        }

        Object.assign(filters, this.filters);

        return filters;
    }

    extractRequestFields(req) {
        let searchFor = ['fields'];
        let fields = null;

        for(let i = 0; i < searchFor.length; i++) {
            let key = searchFor[i];
            let dashkey = '_' + searchFor[i];

            if(key in req.query) {
                fields = req.query[key];

            } else if(dashkey in req.query) {
                fields = req.query[dashkey];
            }
        }

        return fields ? fields.split(/\s*,\s*/) : null;
    }

    extractRequestLimit(req) {
        let searchFor = ['limit', 'end'];
        let limit = this.options.limit;

        for(let i = 0; i < searchFor.length; i++) {
            let key = searchFor[i];
            let dashkey = '_' + searchFor[i];

            if(key in req.query) {
                limit = parseInt(req.query[key]);

            } else if(dashkey in req.query) {
                limit = parseInt(req.query[dashkey]);
            }
        }

        return limit <= 0 ? this.options.limit : limit;
    }

    extractRequestOffset(req) {
        let searchFor = ['start', 'offset', 'from'];
        let offset = 0;

        for(let i = 0; i < searchFor.length; i++) {
            let key = searchFor[i];
            let dashkey = '_' + searchFor[i];

            if(key in req.query) {
                offset = parseInt(req.query[key]);

            } else if(dashkey in req.query) {
                offset = parseInt(req.query[dashkey]);
            }
        }

        return offset;
    }

    extractRequestParams(req) {
        let params = {};

        for(let key in req.body) {
            if(!req.body.hasOwnProperty(key)) {
                continue;
            }

            params[key] = this.convertValueToNative(req.body[key])
        }

        return params;
    }

    /* -------------------------------------------------------------------------
     * Utils
     * ---------------------------------------------------------------------- */

    convertValueToNative(value) {
        return value
    }

    convertOperator(operator) {
        /*
         $and: {a: 5}           // AND (a = 5)
         $or: [{a: 5}, {a: 6}]  // (a = 5 OR a = 6)
         $gt: 6,                // > 6
         $gte: 6,               // >= 6
         $lt: 10,               // < 10
         $lte: 10,              // <= 10
         $ne: 20,               // != 20
         $eq: 3,                // = 3
         $not: true,            // IS NOT TRUE
         $between: [6, 10],     // BETWEEN 6 AND 10
         $notBetween: [11, 15], // NOT BETWEEN 11 AND 15
         $in: [1, 2],           // IN [1, 2]
         $notIn: [1, 2],        // NOT IN [1, 2]
         $like: '%hat',         // LIKE '%hat'
         $notLike: '%hat'       // NOT LIKE '%hat'
         $iLike: '%hat'         // ILIKE '%hat' (case insensitive) (PG only)
         $notILike: '%hat'      // NOT ILIKE '%hat'  (PG only)
         $like: { $any: ['cat', 'hat']}
         // LIKE ANY ARRAY['cat', 'hat'] - also works for iLike and notLike
         $overlap: [1, 2]       // && [1, 2] (PG array overlap operator)
         $contains: [1, 2]      // @> [1, 2] (PG array contains operator)
         $contained: [1, 2]     // <@ [1, 2] (PG array contained by operator)
         $any: [2,3]            // ANY ARRAY[2, 3]::INTEGER (PG only)
         */

        // TODO: validate

        switch(operator) {
            case '=':
                return '$eq';
            case '>':
                return '$gt';
            case '>=':
                return '$gte';
            case '<':
                return '$lt';
            case '<=':
                return '$lte';
            case '!=':
                return '$ne';
        }

        return operator;
    }

    getPrimaryKey() {
        return this
            .model()
            .then(Model => {
                return Model.getPrimaryKey() || 'id';
            });
    }

    applySort(rows, order) {
        rows.sort((rowA, rowB) => {
            for(let i = 0; i < order.length; i++) {
                let orderNode = order[i];
                let column = orderNode[0];
                let dir = orderNode[1].toLowerCase() == 'asc' ? 1 : -1;

                let valA = __.object.queryGet(rowA, column);
                let valB = __.object.queryGet(rowB, column);


                if(valA > valB) return dir < 0 ? -1 : +1;
                if(valA < valB) return dir < 0 ? +1 : -1;
            }

            return 0;
        });

        return rows;
    }

    /* -------------------------------------------------------------------------
     * Output
     * ---------------------------------------------------------------------- */

    applyTransformations(result) {
        if(_.isArray(result)) {
            // TODO: apply sort
            return result.map(v => this.applyTransformations(v));
        }

        if(this.options.fields.length > 0) {
            let clean = {};

            if(typeof result != 'object') {
                return result;
            }

            this.options.fields.forEach(field => {
                if(field in result) {
                    clean[field] = result[field];

                } else {
                    clean[field] = null;
                }
            });

            return clean;
        }

        return result;
    }

    outputSuccess(res, result) {
        res.status(200).json(__.object.simplify(result));
        return this;
    }

    outputError(res, err) {
        let errCode = 500;

        if(err.code == 404) {
            errCode = 404;
        }

        res.status(errCode).json(this.formatError(err));
        return this;
    }

    formatError(err) {
        return {
            error: err.message,
            name: err.name,
            filename: err.fileName,
            line: err.lineNumber,
            column: err.columnNumber,
            stack: err.stack ? err.stack.split(/\n/) : []
        };
    }
}

module.exports = RestController;