'use strict';

// local requires
var express         = require('express');
var helmet          = require('helmet');
var session         = require('express-session');
var morgan          = require('morgan');
var path            = require('path');
var http            = require('http');
var bodyParser      = require('body-parser');
var cookieParser    = require('cookie-parser');
var methodOverride  = require('method-override');
var authentication  = require('../authentication/Authentication');
var socket          = require('../socket/SocketServer');
var utilBanner      = require('../utils/UtilBanner');
var utilString      = require('../../shared/utils/UtilString');
var staticServer    = require('./StaticServer');
var DistributedLock = require('../lock/DistributedLock');
var webAppLogger    = require('../utils/Logger').loggers.get('webAppError');
var logConfig       = require('../configLoader/LogConfig.js');
const { createTerminus } = require('@godaddy/terminus');

function exitOnError(exitErrorMsg) {
    logger.error(exitErrorMsg);
    utilBanner.printCompletionBanner("Application shutdown with errors");
    process.exit();
}

module.exports.start = function () {
    global.logger = require('../utils/Logger').loggers.get('main');
    global.DataService = require('../database/DataService');
    var app = express();

    app.set('host', config.get('host') || 'localhost');
    var appPort = config.get('port');
    appPort = parseInt(appPort);

    // NODE_APP_INSTANCE will be defined when running in a clustered environment using pm2. If this is set, add the
    // NODE_APP_INSTANCE to the port value so that each instance will run on a different port. So if the port setting is 3000 and
    // there are four instances in the cluster, there will be instances running on ports 3000, 3001, 3002, 3003.
    // The reverse proxy, usually nginx, will be set to point at each of these ports so requests to the nginx server will direct
    // the request to the different ports. An example nginx config is in system/nginx/conf.d/formbird.com.conf
    if (process.env.NODE_APP_INSTANCE) {
        if (process.env.NO_PORT_INCREMENT !== 'true'){
          appPort += parseInt(process.env.NODE_APP_INSTANCE);
        }
        logger.info("Using port " + appPort + " for app instance " + process.env.NODE_APP_INSTANCE);
    }

    app.set('port', appPort);

    const accessLog = logConfig.get("accessLog") || {};
    const morganOptions = accessLog.morganOptions || {};
    morganOptions.stream = {
        write: (message) => {
           webAppLogger.error(message);
        }
    };

    if (accessLog.logMessages === "error") {
        morganOptions.skip = function (req, res) { 
            return res.statusCode === 200 || res.statusCode === 304;
        };
    }

    app.use(morgan(accessLog.format || 'dev', morganOptions));
    app.use(helmet());

    app.use(bodyParser.urlencoded({
        extended: true
    }));

    var maxUploadSize = (parseInt(config.get('maxUploadSize')) ?
      parseInt(config.get('maxUploadSize')) : SharedConstants.DEFAULT_MAX_UPLOAD_SIZE) + "MB";

    app.use(bodyParser.json({ limit: maxUploadSize }));

    require('../session/Session')(onSessionConfigFetched);

    function onSessionConfigFetched(err, sessionConfig) {

        if (err) {
            exitOnError(err.message);
            return;
        }

        //use secured cookies on production
        if (app.get('env') === 'production') {
            app.set('trust proxy', 1); // trust first proxy
            sessionConfig.cookie.secure = true; // serve secure cookies
        }

        // set up authentication
        app.use(session(sessionConfig));
        app.use(authentication.initialize());
        app.use(authentication.session());
        app.use(cookieParser());

        //this is used to prevent IE11/Microsoft Edge from caching requests.
        app.use(function noCache(req, res, next) {
            res.header("Expires", 0);
            next();
        });

        // set up the serving of static files
        var basePath = require('./BaseURLPath').basePathWithSlashPrefix;
        if (basePath !== "/" && (basePath.indexOf("//") === 0 || basePath.lastIndexOf("/") === basePath.length - 1)) {
             exitOnError("Base URL Path may not start or end with a '/'");
        } else {
            var checkAuthentication = require('../controllers/AuthenticationController').checkAuthentication;

            let authCheckPath = '/api';
            if (basePath !== "/") {
                authCheckPath = basePath + authCheckPath;
            }

            app.use(authCheckPath, checkAuthentication);
            // end set up authentication

            app.use(methodOverride());

            staticServer.setupStaticServer(app).then(function successFunc() {

                logger.info("Initialising data service");

                // connect to Data Service
                DataService.initialise().then(function () {
                    logger.info("Setting up server url routes");

                    require('./ServerUrlRoutes')(app);

                    logger.info("Starting web server");
                    var port = app.get('port');
                    var host = app.get('host');
                    var server = http.createServer(app).listen(port, host, function () {
                        utilBanner.printStartCompleteBanner(port);
                    });

                    let handleUncaughtExceptions = config.get('handleUncaughtExceptions') !== false;
                    let delayInMillis = config.get('shutdownTimeoutDelayMilliseconds') || 2000;
                    
                    const httpConf = config.get('http');
                    if (httpConf && httpConf.requestTimeoutMilliseconds) {
                        delayInMillis += httpConf.requestTimeoutMilliseconds;
                        server.setTimeout(httpConf.requestTimeoutMilliseconds);
                    }

                    DistributedLock.initialiseLock();

                    var gracefulExit = async () => {
                        await killSignal();
                        process.exit();
                    };
                  
                    var shutDownApp = (resolve) => {
                        
                        closeAllConnections();
                            
                        logger.info("Closing server...");
                        server.close((err) => {
                            if (err) { 
                                logger.info("Error on closing server: " + err); 
                                resolve();
                            } else {
                                socket.close().then(function successFunc() {
                                    logger.info("Disconnected all sockets. Closing Data Service...");
                                    DataService.close();
                                }, function errorFunc(err) {
                                    logger.error("Error on closing socket: " + err);
                                    resolve();
                                });
                            }
                        });
                    };

                    const killSignal = () => {
                        
                        if (handleUncaughtExceptions) {
                            logger.info("Received kill signal, shutting down gracefully in " + delayInMillis / 1000 + " seconds.");
                        } else {
                            logger.info('Received kill signal, shutting down gracefully');
                        }

                        return new Promise((resolve) => {
                         
                          if (handleUncaughtExceptions) {
                              setTimeout(() => {
                                  shutDownApp(resolve);
                              }, delayInMillis);
                         
                          } else {
                              shutDownApp(resolve);
                          }
                        });
                    };
                  
                    const connectionMap = {};
                    let lastConnectionKey = 0;
                    server.on('connection', function(connection) {
                        const connectionKey = ++lastConnectionKey;
                        connectionMap[lastConnectionKey] = connection;
                        connection.on('close', function() {
                            delete connectionMap[connectionKey];
                        });
                    });

                    function closeAllConnections() {
                        logger.info('Closing all server connections...');
                      
                        Object.keys(connectionMap).forEach(function (connectionKey) {
                          try {
                            connectionMap[connectionKey].destroy();
                          } catch (e) {
                            logger.info('Error on connection destruction');
                          }
                        });
                    }

                    // Respond with a 404 to failed GET requests for missing javascript files.
                    // We need to respond with the appropriate http status (404) if the client requests for javascript files that
                    // the StaticServer.js do not support. This is so that the service worker will not cache wrong content.
                    // If this is not present, the request will fall back to the '*' GET handler whose response will be cached by
                    // the service worker.
                    // See the fallback below: app.get('*' ...)
                    app.get('*.js', function (req, res, next) {
                        res.status(404).send('Not found');
                    });

                    // make sure the * handler is last to register; fallback into index.html
                    app.get('*', function (req, res, next) {
                        res.sendFile(path.resolve(__dirname + '/../public/index.html'));
                    });

                    socket.listen(server, app);

                    // uncomment the following to view the available rest apis in the log
                    // const listEndpoints = require('express-list-endpoints');
                    // const endPoints = listEndpoints(app);
                    // const longest = endPoints.reduce(function (a, b) { return a.path.length > b.path.length ? a : b; });
                    // endPoints.sort((a, b) => { return a.path.localeCompare(b.path); });
                    // const padding = longest.path.length;
                    // for(const route of endPoints){
                    //     // eslint-disable-next-line no-console
                    //     console.log(`${route.path.padEnd(padding, ' ')} - ${route.methods}`);
                    // }

                    //graceful exit process handler
                    // listen for TERM signal .e.g. kill
                    process.on('SIGTERM', gracefulExit);
                    // listen for INT signal e.g. Ctrl-C
                    createTerminus(server, {
                        beforeShutdown: killSignal,
                        signal: 'SIGINT',
                        useExit0: true
                    });

                    if (handleUncaughtExceptions) {
                        process.on('unhandledRejection', (reason) => {
                            logger.error('Unhandled Rejection at Promise: ' + reason);
                            gracefulExit();
                        });
                      
                        process.on('uncaughtException', (err) => {
                            logger.error('Uncaught Exception thrown: ' + err);
                            gracefulExit();
                        });
                    }

                }, function error(err) {
                    logger.error("Failed to initialize DataService: " + err);
                    utilBanner.printCompletionBanner("Application shutdown with errors");
                    process.exit();
                });
            }, function errorFunc(err) {
                logger.error("Error setting up static server: " + err);
                utilBanner.printCompletionBanner("Application shutdown with errors");
                process.exit();
            });
        }

    }

};
