'use strict';

const _ = require('lodash');

const ControllerRequest = require('./controller-request');
const ControllerAudit = require('./controller-audit');
const ControllerSummoner = require('./controller-summoner');

const asyncp = require('paradox-asyncp');
const logger = require('paradox-logger')('controller-manager');
const EventEmitter = require('paradox-utils').EventEmitter;
const __ = require('paradox-utils');

const Promise = require('bluebird');

class ControllerManager extends EventEmitter {

    constructor(options) {
        super();

        this.options = _.defaults(options, {
            cluster: null,
            io: null,
            paths: ['./controllers'],
        });

        this.controller = new ControllerSummoner(this, options.cluster, options.paths, options);
        this.audit = new ControllerAudit(this);
    }


    /* -------------------------------------------------------------------------
     * Call
     * ---------------------------------------------------------------------- */

    /**
     * @returns {ControllerSummoner}
     */
    getSummoner() {
        return this.controller;
    }

    /**
     * @param {ControllerRequest} request
     * @param options
     * @returns {Promise}
     */
    call(request, options) {
        return this.callController(request.controller, request.action, request, options);
    }

    callController(controller, action, request, options) {
        options = _.defaults(options, this.options, {
            retry: 1,
            paths: './controllers'
        });

        request.controller = controller;
        request.action = action;

        request = ControllerRequest.factoryObject(request);

        let originalQuery = _.cloneDeep(request.query);

        return this
            .emit('call', [controller, action, request, options])
            .then(resp => {
                if(typeof resp !== 'undefined') {
                    return resp;
                }

                return asyncp
                    .retry({
                        times: options.retry,
                        errorFilter: (err) => {
                            if(err && err.retriable === false) {
                                return false;
                            }

                            return true;
                        }

                    }, (tryNumber, results, err) => {
                        if(tryNumber > 1) {
                            logger.error('Retrying after error... (' + (err ? err.message : 'no error') + ')');
                            request.query = _.cloneDeep(originalQuery);
                        }

                        if(options.cluster && options.cluster.isMaster()) {
                            return this
                                .controller
                                .callActionCluster(controller, action, request, options);
                        }

                        return this
                            .controller
                            .callAction(controller, action, request, options);
                    })
                    .then(result => {
                        result = __.object.simplify(result);

                        // Log ok
                        this.audit.logSuccess(request, result);
                        return result;
                    })
                    .catch(err => {
                        // Log error
                        this.audit.logFail(request, err);
                        return Promise.reject(err);
                    });
            });

    }

    /* -------------------------------------------------------------------------
     * Controller middleware
     * ---------------------------------------------------------------------- */

    registerMiddleware(router, controller) {
        let ctrl = this
            .controller
            .getController(controller);

        return ctrl
            .preload()
            .then(() => {
                return ctrl.register(router, controller);
            });

    }

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

    registerRouter(router) {
        let processReq = (req, res, next) => {
            if(!req.isAuthenticated()) {
                return next();
            }

            if(req.headers['content-type'] && req.headers['content-type'].match(/^multipart/)) {
                ControllerRequest
                    .factoryMultipart(req)
                    .then(request => {
                        this.proxyCall(req, res, next, request);
                    })
                    .catch(err => {
                        if(err && err.type == 'controller' && err.code == 404) {
                            next();

                        } else {
                            this.proxySendError(res, err);
                        }
                    });

            } else {
                let request = ControllerRequest.factory(req);
                this.proxyCall(req, res, next, request);
            }
        };

        router.all('/:namespace/:controller/:action', processReq);
        router.all('/:controller/:action', processReq);
    }

    transformResultNode(node) {
        if(node) {
            if(typeof node == 'object' && node.constructor && node.constructor.name == 'Key') {
                node = node.id;
            }

            if(typeof node.toArray != 'undefined') {
                node = node.toArray();
            }

            if(typeof node.getValues != 'undefined') {
                node = node.getValues();
            }
        }

        return node;
    }

    proxyCall(req, res, next, request, options) {
        options = options || {};

        let logHolder = null;
        let debug = req.query.debug;

        debug = false;

        if(debug) {
            logHolder = logger.createHolder();
        }

        let end = logger.trace('API call received: ' + req.path);

        this
            .call(request, options)
            .then((result) => {
                end();

                let logs = undefined;

                if(debug) {
                    logs = logHolder.getLogs();
                }

                // Output ok
                res.status(200).json({
                    success: true,
                    result: result,
                    request: {
                        session: request.session,
                        elapsed: request.elapsed() / 1000,
                        logs
                    }
                });
            })
            .catch((err) => {
                end();

                if(err && err.type == 'controller' && err.code == 404) {
                    next();
                    return;
                }

                logger.exception(err);

                let statusCode = 500;
                let logs = undefined;

                if(debug) {
                    logs = logHolder.getLogs();
                }

                if(err instanceof Error) {
                    err = {
                        success: false,
                        error: {
                            code: err.code,
                            type: err.type,
                            message: err.message,
                            trace: (err.stack + "").split(/\n+/),
                        },
                        request: {
                            session: request.session,
                            elapsed: request.elapsed() / 1000,
                            logs,
                        }
                    };
                }

                // Output error
                res.status(statusCode).json(err);
            });
    }

    proxySendError(res, err) {
        logger.exception(err);

        let statusCode = 500;
        let logs = undefined;

        if(err instanceof Error) {
            err = {
                success: false,
                error: {
                    code: err.code,
                    type: err.type,
                    message: err.message,
                    trace: (err.stack + "").split(/\n+/),
                },
                request: {
                    session: null,
                    elapsed: null,
                    logs,
                }
            };
        }

        // Output error
        res.status(statusCode).json(err);
    }
}

module.exports = ControllerManager;