'use strict';

const Hapi = require('@hapi/hapi');
const logger = require('screwdriver-logger');
const registerPlugins = require('./registerPlugins');

process.on('unhandledRejection', (reason, p) => {
    console.error('Unhandled Rejection at: Promise', p, 'reason:', reason); /* eslint-disable-line no-console */
});

/**
 * If we're throwing errors, let's have them say a little more than just 500
 * @method prettyPrintErrors
 * @param  {Hapi.Request}    request Hapi Request object
 * @param  {Hapi.h}     h   Hapi Response Toolkit
 */
function prettyPrintErrors(request, h) {
    const { response } = request;
    const { release } = request.server.app;

    if (release && release.cookieName && request.state && !request.state[release.cookieName]) {
        h.state(release.cookieName, release.cookieValue);
    }

    if (!response.isBoom) {
        return h.continue;
    }

    const err = response;
    const errName = err.output.payload.error;
    const errMessage = err.message;
    const { statusCode } = err.output.payload;
    const stack = err.stack || errMessage;

    if (statusCode === 500) {
        request.log(['server', 'error'], stack);
    }

    const res = {
        statusCode,
        error: errName,
        message: errMessage
    };

    if (err.data) {
        res.data = err.data;
    }

    return h.response(res).code(statusCode);
}

/**
 * Configures & starts up a HapiJS server
 * @method
 * @param  {Object}      config
 * @param  {Object}      config.httpd
 * @param  {Integer}     config.httpd.port          Port number to listen to
 * @param  {String}      config.httpd.host          Host to listen on
 * @param  {String}      config.httpd.uri           Public routable address
 * @param  {Object}      config.httpd.tls           TLS Configuration
 * @param  {Object}      config.webhooks            Webhooks settings
 * @param  {String}      config.webhooks.restrictPR Restrict PR setting
 * @param  {Boolean}     config.webhooks.chainPR    Chain PR flag
 * @param  {Object}      config.ecosystem           List of hosts in the ecosystem
 * @param  {Object}      config.ecosystem.ui        URL for the User Interface
 * @param  {Factory}     config.pipelineFactory     Pipeline Factory instance
 * @param  {Factory}     config.jobFactory          Job Factory instance
 * @param  {Factory}     config.userFactory         User Factory instance
 * @param  {Factory}     config.bannerFactory       Banner Factory instance
 * @param  {Factory}     config.buildFactory        Build Factory instance
 * @param  {Factory}     config.buildClusterFactory Build Cluster Factory instance
 * @param  {Factory}     config.stepFactory         Step Factory instance
 * @param  {Factory}     config.secretFactory       Secret Factory instance
 * @param  {Factory}     config.tokenFactory        Token Factory instance
 * @param  {Factory}     config.eventFactory        Event Factory instance
 * @param  {Factory}     config.collectionFactory   Collection Factory instance
 * @param  {Factory}     config.stageFactory        Stage Factory instance
 * @param  {Factory}     config.stageBuildFactory   Stage Build Factory instance
 * @param  {Factory}     config.triggerFactory      Trigger Factory instance
 * @param  {Object}      config.builds              Config to include for builds plugin
 * @param  {Object}      config.builds.ecosystem    List of hosts in the ecosystem
 * @param  {Object}      config.builds.authConfig   Configuration for auth
 * @param  {Object}      config.builds.externalJoin Flag to allow external join
 * @param  {Object}      config.unzipArtifactsEnabled  Flag to allow unzip artifacts
 * @param  {Object}      config.artifactsMaxDownloadSize Maximum download size for artifacts
 * @param  {Function}    callback                   Callback to invoke when server has started.
 * @return {http.Server}                            A listener: NodeJS http.Server object
 */

module.exports = async config => {
    try {
        // Hapi Cross-origin resource sharing configuration
        // See http://hapijs.com/api for available options

        let corsOrigins = [config.ecosystem.ui];

        if (Array.isArray(config.ecosystem.allowCors)) {
            corsOrigins = corsOrigins.concat(config.ecosystem.allowCors);
        }

        const cors = {
            origin: corsOrigins,
            additionalExposedHeaders: ['x-more-data'],
            credentials: true
        };
        // Create a server with a host and port
        const server = new Hapi.Server({
            port: config.httpd.port,
            host: config.httpd.host,
            uri: config.httpd.uri,
            routes: {
                cors,
                log: { collect: true }
            },
            state: {
                strictHeader: false
            },
            router: {
                stripTrailingSlash: true
            }
        });

        // Set the factories within server.app
        // Instantiating the server with the factories will apply a shallow copy
        server.app = {
            commandFactory: config.commandFactory,
            commandTagFactory: config.commandTagFactory,
            templateFactory: config.templateFactory,
            templateTagFactory: config.templateTagFactory,
            pipelineTemplateFactory: config.pipelineTemplateFactory,
            pipelineTemplateVersionFactory: config.pipelineTemplateVersionFactory,
            jobTemplateTagFactory: config.jobTemplateTagFactory,
            pipelineTemplateTagFactory: config.pipelineTemplateTagFactory,
            stageFactory: config.stageFactory,
            stageBuildFactory: config.stageBuildFactory,
            triggerFactory: config.triggerFactory,
            pipelineFactory: config.pipelineFactory,
            jobFactory: config.jobFactory,
            userFactory: config.userFactory,
            buildFactory: config.buildFactory,
            stepFactory: config.stepFactory,
            bannerFactory: config.bannerFactory,
            secretFactory: config.secretFactory,
            tokenFactory: config.tokenFactory,
            eventFactory: config.eventFactory,
            collectionFactory: config.collectionFactory,
            buildClusterFactory: config.buildClusterFactory,
            ecosystem: config.ecosystem,
            release: config.release,
            queueWebhook: config.queueWebhook,
            unzipArtifacts: config.unzipArtifactsEnabled
        };

        const bellConfigs = await config.auth.scm.getBellConfiguration();

        config.auth.bell = bellConfigs;

        if (config.release && config.release.cookieName) {
            server.state(config.release.cookieName, {
                path: '/',
                ttl: config.release.cookieTimeout * 60 * 1000, // (2 mins)
                isSecure: config.auth.https,
                isHttpOnly: !config.auth.https
            });
        }

        // Write prettier errors
        server.ext('onPreResponse', prettyPrintErrors);

        // Audit log
        if (config.log && config.log.audit.enabled) {
            server.ext('onCredentials', (request, h) => {
                const { username, scope, pipelineId } = request.auth.credentials;

                if (scope) {
                    const validScope = config.log.audit.scope.filter(s => scope.includes(s));

                    if (Array.isArray(validScope) && validScope.length > 0) {
                        let context;

                        if (validScope.includes('admin')) {
                            context = `Admin ${username}`;
                        } else if (validScope.includes('user')) {
                            context = `User ${username}`;
                        } else if (validScope.includes('build') || validScope.includes('temporal')) {
                            context = `Build ${username}`;
                        } else if (validScope.includes('pipeline')) {
                            context = `Pipeline ${pipelineId}`;
                        } else {
                            context = `Guest ${username}`;
                        }

                        logger.info(`[Login] ${context} ${request.method} ${request.path}`);
                    }
                }

                return h.continue;
            });
        }

        // Register events for notifications plugin
        server.event(['build_status', 'job_status']);

        // Register plugins
        await registerPlugins(server, config);

        // Initialize common data in buildFactory and jobFactory
        server.app.buildFactory.apiUri = server.info.uri;
        server.app.buildFactory.tokenGen = (buildId, metadata, scmContext, expiresIn, scope = ['temporal']) =>
            server.plugins.auth.generateToken(
                server.plugins.auth.generateProfile({ username: buildId, scmContext, scope, metadata }),
                expiresIn
            );
        server.app.buildFactory.executor.tokenGen = server.app.buildFactory.tokenGen;
        server.app.buildFactory.maxDownloadSize = parseInt(config.artifactsMaxDownloadSize, 10) * 1024 * 1024 * 1024;

        server.app.jobFactory.apiUri = server.info.uri;
        server.app.jobFactory.tokenGen = (username, metadata, scmContext, scope = ['user']) =>
            server.plugins.auth.generateToken(
                server.plugins.auth.generateProfile({ username, scmContext, scope, metadata })
            );
        server.app.jobFactory.executor.userTokenGen = server.app.jobFactory.tokenGen;

        if (server.plugins.shutdown) {
            server.plugins.shutdown.handler({
                taskname: 'executor-queue-cleanup',
                task: async () => {
                    await server.app.jobFactory.cleanUp();
                    logger.info('completed clean up tasks');
                }
            });
        }

        // Start the server
        await server.start();

        return server;
    } catch (err) {
        logger.error('Failed to start server', err);
        throw err;
    }
};