'use strict';

const asyncp = require('paradox-asyncp');
const _ = require('lodash');
const Raven = require('raven');
const express = require('express');
const http = require('http');
const cors = require('cors');
const methodOverride = require('method-override');
const basicAuth = require('basic-auth');
const bodyParser = require('body-parser');
const SocketIo = require('socket.io');
const compression = require('compression');
const ControllerManager = require('./app/controller-manager');
const CloudManager = require('./app/cloud-manager');
const path = require('path');
const paradoxAdmin = require('paradox-ng-admin');
const ClusterManager = require('./app/cluster-manager');
const ApiManager = require('./app/api-manager');
const Promise = require('bluebird');
const LoginManager = require('./app/login-manager');
const requestIp = require('request-ip');

const logger = require('paradox-logger')('app');

const fsUtils = require('paradox-utils').fs;
const EventEmitter = require('events').EventEmitter;

class App extends EventEmitter {

    constructor(options) {
        super();

        this.express_app = null;
        this.express_server = null;
        this.io = null;
        this.pmx = null;
        this.cluster = null;
        this.controller_manager = null;
        this.login_manager = null;
        this.cloudInstances = {
            sql: null,
            datastore: null,
            bigquery: null,
            storage: null,
            elasticsearch: null,
            nedb: null,
        };

        this.startPromise = null;

        this.options = _.defaultsDeep(options, {
            basedir: './',
            express_port: 0,
            express_password_regex: null,
            express_password_regex_exclude: null,
            express_user: null,
            express_password: null,
            express_compression: true,
            express_cors: true,
            raven_token: null,
            views_path: null,
            auto_routing: true,
            pmx: false,
            router: {},
            static: {},
            controller: {},
            controller_manager: {
                retry: 1,
                ignoreCluster: [],
                timeout: null,
            },
            api: {
                prefix: '/api',
                snippets: {
                    curl: require('./app/snippets/curl-snippet'),
                    php: require('./app/snippets/php-snippet'),
                    python: require('./app/snippets/python-snippet'),
                    jquery: require('./app/snippets/jquery-snippet'),
                    nodejs: require('./app/snippets/nodejs-snippet'),
                    ruby: require('./app/snippets/ruby-snippet'),
                    java: require('./app/snippets/java-snippet'),
                    csharp: require('./app/snippets/csharp-snippet'),
                },
                endpoints: [],
            },
            middleware: {},
            admin: null,
            cloud: {
                bigquery: null,
                datastore: null,
                sql: null,
                storage: null,
                pubsub: null,
                elasticsearch: null,
                nedb: {
                    models: __dirname + '/app/models'
                },
            },
            cluster: {
                enabled: false,
                type: 'master',
                master_address: null,
            },
            login: {
                enabled: false,
                anonymous: {
                    token: 'anonymous',
                    key: 'anonymous',
                    enabled: true,
                    username: 'anonymous',
                    password: null,
                    email: null,
                },
                loginEverywhere: false,
                store: 'nedb',
                users: [],
                tokens: [],
                partition: false,
                models: {
                    user: 'user',
                    token: 'user-token',
                    session: 'user-session',
                    history: 'user-history',
                },
            }
        });
    }

    /* -------------------------------------------------------------------------
     * Start
     * ---------------------------------------------------------------------- */

    start() {
        if(this.startPromise) {
            return this.startPromise;
        }

        let standaloneMode = process.env.STANDALONE_MODE || false;
        let selectiveMode = (process.env.SELECTIVE_MODE || '');

        let toStart = [
            {key: 'raven', fnc: 'startRaven'},
            {key: 'pmx', fnc: 'startPmx'},
            {key: 'cloud', fnc: 'startCloud'},
            {key: 'server', fnc: 'startExpress'},
            {key: 'sockets', fnc: 'startSocketIo'},
            {key: 'cluster', fnc: 'startCluster'},
            {key: 'api', fnc: 'startApi'},
            {key: 'constrollers', fnc: 'startControllers'},
            {key: 'admin', fnc: 'startAdmin'},
            {key: 'end', fnc: 'end'},
        ];

        if(standaloneMode) {
            toStart = [
                {key: 'cloud', fnc: 'startCloud'},
            ];

        } else if(selectiveMode) {
            selectiveMode = selectiveMode.split(/\s*,\s*/g);

            if(selectiveMode.length > 0) {
                toStart = toStart.filter(n => selectiveMode.includes(n.key));
            }
        }

        return this.startPromise = asyncp
            .eachLimit(toStart, 1, node => {
                let end = logger.trace('[APP] Starting: ' + node.key);

                return Promise
                    .try(() => {
                        return this[node.fnc].apply(this);
                    })
                    .finally(() => {
                        end();
                        this.emit('load-' + node.key);
                    });
            })
            .then(() => {
                this.emit('load');
            })
            .catch(err => {
                logger.exception(err);
                return Promise.reject(err);
            });
    }

    /* -------------------------------------------------------------------------
     * Parts
     * ---------------------------------------------------------------------- */

    startRaven() {
        return new Promise((resolve, reject) => {
            if(this.options.raven_token) {
                Raven.config(this.options.raven_token).install();
            }

            resolve();
        });
    }

    startPmx() {
        return new Promise((resolve, reject) => {
            let pmx = null;

            if(this.options.pmx) {
                pmx = {
                    enabled: false,
                    emit: () => {
                    },
                    action: () => {
                    },
                    scopedAction: () => {
                    },
                    notify: () => {
                    },
                    probe: () => {
                        return {
                            metric: () => {
                                return {
                                    set: () => {
                                    },
                                }
                            },
                            counter: () => {
                                return {
                                    inc: () => {
                                    },
                                    dec: () => {
                                    },
                                }
                            },
                            meter: () => {
                                return {
                                    mark: () => {
                                    }
                                }
                            },
                            histogram: () => {
                                return {
                                    update: () => {
                                    }
                                }
                            }
                        }
                    },
                    expressErrorHandler: () => {
                    },
                    init: () => {
                    },
                    http: () => {
                    },
                };

            } else {
                pmx = require('pmx').init({
                    http: true, // HTTP routes logging (default: true)
                    ignore_routes: [/socket\.io/, /notFound/], // Ignore http routes with this pattern (Default: [])
                    errors: true, // Exceptions loggin (default: true)
                    custom_probes: true, // Auto expose JS Loop Latency and HTTP req/s as custom metrics
                    network: true, // Network monitoring at the application level
                    ports: true  // Shows which ports your app is listening on (default: false)
                });

                pmx.http && pmx.http(); // TODO: review
                pmx.enabled = true;
            }

            this.pmx = pmx;
            resolve(pmx);
        });
    }

    startExpress() {
        return new Promise((resolve, reject) => {
            let app = express();
            let server = http.Server(app);

            this.express_app = app;
            this.express_server = server;

            /**
             * Middlewares
             */

            this.emit('express-started');

            // raven
            if(this.options.raven_token) {
                app.use(Raven.requestHandler());
                app.use(Raven.errorHandler());
            }

            // pmx
            if(this.pmx && this.pmx.enabled) {
                app.use(this.pmx.expressErrorHandler());
            }

            app.disable('x-powered-by');
            app.use(methodOverride());
            app.use(bodyParser.urlencoded({extended: false}));
            app.use(bodyParser.json({limit: '50mb'}));
            app.use(requestIp.mw());

            if(this.options.express_cors) {
                app.use(cors());
            }

            if(this.options.express_compression) {
                app.use(compression());
            }

            app.enable('trust proxy');

            if(this.options.views_path) {
                app.set('view engine', 'html');
                app.set('views', this.options.views_path);
                app.set('view cache', false);
            }

            /* -------------------------------------------------------------------------
             * Session
             * ---------------------------------------------------------------------- */

            this.getLoginManager().register(app);

            /* -------------------------------------------------------------------------
             * Password
             * ---------------------------------------------------------------------- */

            /**
             * Server password
             */
            if(this.options.express_user && this.options.express_password) {
                app.use((req, res, next) => {
                    if(this.options.express_password_regex && !req.url.match(this.options.express_password_regex)) {
                        next();
                        return;
                    }

                    if(this.options.express_password_regex_exclude && req.url.match(this.options.express_password_regex_exclude)) {
                        next();
                        return;
                    }

                    let user = basicAuth(req);

                    if(user === undefined || user['name'] !== this.options.express_user || user['pass'] !== this.options.express_password) {
                        res.statusCode = 401;
                        res.setHeader('WWW-Authenticate', 'Basic realm="Access"');
                        res.end('Unauthorized');

                    } else {
                        next();
                    }
                });
            }

            this.emit('express-middleware-loaded');

            /* -------------------------------------------------------------------------
             * Routers
             * ---------------------------------------------------------------------- */

            /**
             * API Router
             */
            for(let uriPath in this.options.router) {
                let router = express.Router();
                let dirPath = path.resolve(this.options.basedir, this.options.router[uriPath]);

                fsUtils.listFolderFiles(dirPath).forEach(file => {
                    require(file).apply(this, [app, router]);
                });

                app.use(uriPath, router);
            }

            /**
             * Default Router
             */
            for(let uriPath in this.options.static) {
                let dirPath = path.resolve(this.options.basedir, this.options.static[uriPath]);
                let staticFnc = express.static(dirPath);

                app.use(uriPath, (req, res, next) => {
                    if(!req.isAuthenticated()) {
                        return next();
                    }

                    staticFnc(req, res, next);
                });
            }

            /**
             * Start
             */

            let port = this.options.express_port;

            this.emit('express-before-listen');

            server.listen(port, () => {
                resolve(server);
            });

            this.emit('express-after-listen');
        });
    }

    end() {
        if(this.express_app) {
            let app = this.express_app;

            this.getLoginManager().registerEnd(this.express_app);

            /**
             * Errors
             */
            app.use(function(err, req, res, next) {
                if(err.status == 400 && err.message.startsWith('Unexpected token')) {
                    res
                        .status(500)
                        .json({
                            message: 'invalid json payload format: ' + err.message,
                        });

                    return;
                }

                logger.info('Unhandled error');
                logger.exception(err);

                res
                    .status(500)
                    .json({
                        message: 'unknown error',
                    });
            });

            /**
             * 404
             */
            app.use(function(req, res, next) {
                res.status(404);

                // respond with html page
                if(req.accepts('html')) {
                    res.sendFile(__dirname + '/app/pages/404.html');
                    return;
                }

                // respond with json
                if(req.accepts('json')) {
                    res.send({message: 'content not found'});
                    return;
                }

                // default to plain-text. send()
                res.type('txt').send('Not found');
            });
        }

        return Promise.resolve();
    }

    startSocketIo() {
        return new Promise((resolve, reject) => {
            if(!this.express_server) {
                return reject(new Error('express not initialized'));
            }

            this.io = SocketIo(this.express_server);

            // TODO: Config auth

            resolve(this.io);
        });
    }

    startCluster() {
        if(!this.options.cluster.enabled) {
            return Promise.resolve();
        }

        this.cluster = new ClusterManager(this.io, this.options.cluster.type, this.options.cluster);

        if(this.cluster.isMaster()) {
            logger.info('Cluster instance is the MASTER');

        } else if(this.cluster.isSlave()) {
            logger.info('Cluster instance is a SLAVE');
        }

        return this
            .cluster
            .start();
    }

    getLoginManager() {
        if(!this.login_manager) {
            this.login_manager = new LoginManager(this.cloud.bind(this), this.options.login);
        }

        return this.login_manager;
    }


    getControllerManager() {
        if(!this.controller_manager) {
            let paths = [];

            for(let uriPath in this.options.controller) {
                paths.push(path.resolve(this.options.basedir, this.options.controller[uriPath]));
            }

            this.controller_manager = new ControllerManager(_.defaults({
                cluster: this.cluster,
                io: this.io,
                paths: paths,
            }, this.options.controller_manager));
        }

        return this.controller_manager;
    }

    startControllers() {
        let manager = this.getControllerManager();

        // Middleware
        for(let uriPath in this.options.middleware) {
            let router = express.Router();

            let controller = this.options.middleware[uriPath];

            if(!_.isArray(controller)) {
                controller = [controller];
            }

            controller.forEach(ctrl => {
                manager.registerMiddleware(router, ctrl);
            });

            this.express_app.use(uriPath, router);
        }

        // Controllers
        if(this.options.auto_routing) {
            for(let uriPath in this.options.controller) {
                let router = express.Router();
                manager.registerRouter(router);
                this.express_app.use(uriPath, router);
            }
        }
    }

    /**
     * @returns {ApiManager}
     */
    getApiManager() {
        if(!this.api_manager) {
            let opts = this.options.api;

            opts.auth_user = this.options.express_user;
            opts.auth_password = this.options.express_password;

            opts.endpoints = opts.endpoints.map(dir => {
                return path.resolve(this.options.basedir, dir);
            });

            opts.basedir = this.options.basedir;

            let controller = this.getControllerManager();
            let server = this.express_app;

            this.api_manager = new ApiManager(controller, server, this.cloud.bind(this), opts);
        }

        return this.api_manager;
    }

    startApi() {
        let apiManager = this.getApiManager();

        apiManager.app = this.express_app;
        return apiManager
            .register()
            .then(() => {
                return this
                    .getLoginManager()
                    .registerApi(apiManager);
            })
    }

    controller(controller) {
        return this.getControllerManager().getSummoner().getController(controller);
    }

    startCloud() {
        for(let adapter in this.options.cloud) {
            let opts = this.options.cloud[adapter];

            if(opts) {
                if(opts.models) {
                    let pathName = opts.models;

                    if(!path.isAbsolute(pathName)) {
                        pathName = path.resolve(this.options.basedir, opts.models);
                    }

                    opts.models = pathName;

                } else {
                    opts.models = path.resolve(this.options.basedir, './models/' + adapter);
                }

                this.cloudInstances[adapter] = new CloudManager(adapter, opts);
            }
        }
    }

    /**
     * @alias db
     * @param adapter
     * @returns {CloudManager}
     */
    cloud(adapter) {
        return this.db(adapter);
    }

    /**
     * @param adapter
     * @returns {CloudManager}
     */
    db(adapter) {
        if(!(adapter in this.cloudInstances)) {
            throw new Error('database "' + adapter + '" not defined');
        }

        if(!this.cloudInstances[adapter]) {
            throw new Error('database "' + adapter + '" not instanciated');
        }

        return this.cloudInstances[adapter];
    }

    startAdmin() {
        if(!this.options.admin || !this.express_app) {
            return;
        }

        this.express_app.use(paradoxAdmin(this.express_app, this.options.admin));
    }
}

App.Controller = require('./app/controller');
App.RestfulController = require('./app/restful-controller');
App.RestController = require('./app/rest-controller');
App.ControllerManager = require('./app/controller-manager');
App.ControllerRequest = require('./app/controller-request');
App.ControllerResponse = require('./app/controller-response');
App.ControllerSummoner = require('./app/controller-summoner');
App.ControllerAudit = require('./app/controller-audit');
App.DbManager = require('./app/cloud-manager');

module.exports = App;