// Copyright 2012 Mark Cavage, Inc.  All rights reserved.

'use strict';

var domain = require('domain');
var EventEmitter = require('events').EventEmitter;
var http = require('http');
var https = require('https');
var util = require('util');

var _ = require('lodash');
var assert = require('assert-plus');
var errors = require('restify-errors');
var mime = require('mime');
var spdy = require('spdy');
var vasync = require('vasync');

var Chain = require('./chain');
var dtrace = require('./dtrace');
var formatters = require('./formatters');
var shallowCopy = require('./utils').shallowCopy;
var upgrade = require('./upgrade');
var deprecationWarnings = require('./deprecationWarnings');

// Ensure these are loaded
var patchRequest = require('./request');
var patchResponse = require('./response');

var http2;

patchResponse(http.ServerResponse);
patchRequest(http.IncomingMessage);

///--- Globals

var sprintf = util.format;
var PROXY_EVENTS = [
    'clientError',
    'close',
    'connection',
    'error',
    'listening',
    'secureConnection'
];

///--- API

/**
 * Creates a new Server.
 *
 * @public
 * @class
 * @param {Object} options  - an options object
 * @param {String} options.name - Name of the server.
 * @param {Boolean} [options.dtrace=false] - enable DTrace support
 * @param {Router} options.router - Router
 * @param {Object} options.log - [bunyan](https://github.com/trentm/node-bunyan)
 * instance.
 * @param {String} [options.url] - Once listen() is called, this will be filled
 * in with where the server is running.
 * @param {String|Buffer} [options.certificate] - If you want to create an HTTPS
 * server, pass in a PEM-encoded certificate and key.
 * @param {String|Buffer} [options.key] - If you want to create an HTTPS server,
 * pass in a PEM-encoded certificate and key.
 * @param {Object} [options.formatters] - Custom response formatters for
 * `res.send()`.
 * @param {Boolean} [options.handleUncaughtExceptions=false] - When true restify
 * will use a domain to catch and respond to any uncaught
 * exceptions that occur in it's handler stack.
 * Comes with significant negative performance impact.
 * [bunyan](https://github.com/trentm/node-bunyan) instance.
 * response header, default is `restify`. Pass empty string to unset the header.
 * @param {Object} [options.spdy] - Any options accepted by
 * [node-spdy](https://github.com/indutny/node-spdy).
 * @param {Object} [options.http2] - Any options accepted by
 * [http2.createSecureServer](https://nodejs.org/api/http2.html).
 * @param {Boolean} [options.handleUpgrades=false] - Hook the `upgrade` event
 * from the node HTTP server, pushing `Connection: Upgrade` requests through the
 *  regular request handling chain.
 * @param {Boolean} [options.onceNext=false] - Prevents calling next multiple
 *  times
 * @param {Boolean} [options.strictNext=false] - Throws error when next() is
 *  called more than once, enabled onceNext option
 * @param {Object} [options.httpsServerOptions] - Any options accepted by
 * [node-https Server](http://nodejs.org/api/https.html#https_https).
 * If provided the following restify server options will be ignored:
 * spdy, ca, certificate, key, passphrase, rejectUnauthorized, requestCert and
 * ciphers; however these can all be specified on httpsServerOptions.
 * @param {Boolean} [options.noWriteContinue=false] - prevents
 *  `res.writeContinue()` in `server.on('checkContinue')` when proxing
 * @param {Boolean} [options.ignoreTrailingSlash=false] - ignore trailing slash
 * on paths
 * @example
 * var restify = require('restify');
 * var server = restify.createServer();
 *
 * server.listen(8080, function () {
 *   console.log('ready on %s', server.url);
 * });
 */
function Server(options) {
    assert.object(options, 'options');
    assert.object(options.log, 'options.log');
    assert.object(options.router, 'options.router');
    assert.string(options.name, 'options.name');
    assert.optionalBool(
        options.handleUncaughtExceptions,
        'options.handleUncaughtExceptions'
    );
    assert.optionalBool(options.dtrace, 'options.dtrace');
    assert.optionalBool(options.socketio, 'options.socketio');
    assert.optionalBool(options.onceNext, 'options.onceNext');
    assert.optionalBool(options.strictNext, 'options.strictNext');

    var self = this;

    EventEmitter.call(this);

    this.onceNext = !!options.onceNext;
    this.strictNext = !!options.strictNext;
    this.preChain = new Chain({
        onceNext: this.onceNext,
        strictNext: this.strictNext
    });
    this.useChain = new Chain({
        onceNext: this.onceNext,
        strictNext: this.strictNext
    });
    this.log = options.log;
    this.name = options.name;
    this.handleUncaughtExceptions = options.handleUncaughtExceptions || false;
    this.router = options.router;
    this.secure = false;
    this.socketio = options.socketio || false;
    this.dtrace = options.dtrace || false;
    this._inflightRequests = 0;

    var fmt = mergeFormatters(options.formatters);
    this.acceptable = fmt.acceptable;
    this.formatters = fmt.formatters;

    if (options.spdy) {
        this.spdy = true;
        this.server = spdy.createServer(options.spdy);
    } else if (options.http2) {
        // http2 module is not available < v8.4.0 (only with flag <= 8.8.0)
        // load http2 module here to avoid experimental warning in other cases
        if (!http2) {
            try {
                http2 = require('http2');
                patchResponse(http2.Http2ServerResponse);
                patchRequest(http2.Http2ServerRequest);
                // eslint-disable-next-line no-empty
            } catch (err) {}
        }

        assert(
            http2,
            'http2 module is not available, ' +
                'upgrade your Node.js version to >= 8.8.0'
        );

        this.http2 = true;
        this.server = http2.createSecureServer(options.http2);
    } else if ((options.cert || options.certificate) && options.key) {
        this.ca = options.ca;
        this.certificate = options.certificate || options.cert;
        this.key = options.key;
        this.passphrase = options.passphrase || null;
        this.secure = true;

        this.server = https.createServer({
            ca: self.ca,
            cert: self.certificate,
            key: self.key,
            passphrase: self.passphrase,
            rejectUnauthorized: options.rejectUnauthorized,
            requestCert: options.requestCert,
            ciphers: options.ciphers,
            secureOptions: options.secureOptions
        });
    } else if (options.httpsServerOptions) {
        this.server = https.createServer(options.httpsServerOptions);
    } else {
        this.server = http.createServer();
    }

    this.router.on('mount', this.emit.bind(this, 'mount'));

    if (!options.handleUpgrades && PROXY_EVENTS.indexOf('upgrade') === -1) {
        PROXY_EVENTS.push('upgrade');
    }
    PROXY_EVENTS.forEach(function forEach(e) {
        self.server.on(e, self.emit.bind(self, e));
    });

    // Now the things we can't blindly proxy
    this.server.on('checkContinue', function onCheckContinue(req, res) {
        if (self.listeners('checkContinue').length > 0) {
            self.emit('checkContinue', req, res);
            return;
        }

        if (!options.noWriteContinue) {
            res.writeContinue();
        }

        self._onRequest(req, res);
    });

    if (options.handleUpgrades) {
        this.server.on('upgrade', function onUpgrade(req, socket, head) {
            req._upgradeRequest = true;
            var res = upgrade.createResponse(req, socket, head);
            self._onRequest(req, res);
        });
    }

    this.server.on('request', this._onRequest.bind(this));

    this.__defineGetter__('maxHeadersCount', function getMaxHeadersCount() {
        return self.server.maxHeadersCount;
    });

    this.__defineSetter__('maxHeadersCount', function setMaxHeadersCount(c) {
        self.server.maxHeadersCount = c;
        return c;
    });

    this.__defineGetter__('url', function getUrl() {
        if (self.socketPath) {
            return 'http://' + self.socketPath;
        }

        var addr = self.address();
        var str = '';

        if (self.spdy) {
            str += 'spdy://';
        } else if (self.secure) {
            str += 'https://';
        } else {
            str += 'http://';
        }

        if (addr) {
            str +=
                addr.family === 'IPv6'
                    ? '[' + addr.address + ']'
                    : addr.address;
            str += ':';
            str += addr.port;
        } else {
            str += '169.254.0.1:0000';
        }

        return str;
    });

    // print deprecation messages based on server configuration
    deprecationWarnings(self);
}
util.inherits(Server, EventEmitter);

module.exports = Server;

///--- Server lifecycle methods

// eslint-disable-next-line jsdoc/check-param-names
/**
 * Gets the server up and listening.
 * Wraps node's
 * [listen()](
 * http://nodejs.org/docs/latest/api/net.html#net_server_listen_path_callback).
 *
 * @public
 * @memberof Server
 * @instance
 * @function   listen
 * @throws   {TypeError}
 * @param    {Number} port - Port
 * @param    {Number} [host] - Host
 * @param    {Function} [callback] - optionally get notified when listening.
 * @returns  {undefined} no return value
 * @example
 * <caption>You can call like:</caption>
 * server.listen(80)
 * server.listen(80, '127.0.0.1')
 * server.listen('/tmp/server.sock')
 */
Server.prototype.listen = function listen() {
    var args = Array.prototype.slice.call(arguments);
    return this.server.listen.apply(this.server, args);
};

/**
 * Shuts down this server, and invokes callback (optionally) when done.
 * Wraps node's
 * [close()](http://nodejs.org/docs/latest/api/net.html#net_event_close).
 *
 * @public
 * @memberof Server
 * @instance
 * @function   close
 * @param    {Function}  [callback] - callback to invoke when done
 * @returns  {undefined} no return value
 */
Server.prototype.close = function close(callback) {
    if (callback) {
        assert.func(callback, 'callback');
    }

    this.server.once('close', function onClose() {
        return callback ? callback() : false;
    });

    return this.server.close();
};

///--- Routing methods

/**
 * Server method opts
 * @typedef  {String|Regexp |Object} Server~methodOpts
 * @type     {Object}
 * @property {String} name a name for the route
 * @property {String} path can be any String accepted by
 * [find-my-way](https://github.com/delvedor/find-my-way)
 * @example
 * // a static route
 * server.get('/foo', function(req, res, next) {});
 * // a parameterized route
 * server.get('/foo/:bar', function(req, res, next) {});
 * // a regular expression
 * server.get('/example/:file(^\\d+).png', function(req, res, next) {});
 * // an options object
 * server.get({
 *     path: '/foo',
 * }, function(req, res, next) {});
 */

/**
 * Mounts a chain on the given path against this HTTP verb
 *
 * @public
 * @memberof Server
 * @instance
 * @function get
 * @param   {Server~methodOpts} opts - if string, the URL to handle.
 *                                 if options, the URL to handle, at minimum.
 * @returns {Route}                the newly created route.
 * @example
 * server.get('/', function (req, res, next) {
 *    res.send({ hello: 'world' });
 *    next();
 * });
 */
Server.prototype.get = serverMethodFactory('GET');

/**
 * Mounts a chain on the given path against this HTTP verb
 *
 * @public
 * @memberof Server
 * @instance
 * @function head
 * @param   {Server~methodOpts} opts - if string, the URL to handle.
 *                                 if options, the URL to handle, at minimum.
 * @returns {Route}                the newly created route.
 */
Server.prototype.head = serverMethodFactory('HEAD');

/**
 * Mounts a chain on the given path against this HTTP verb
 *
 * @public
 * @memberof Server
 * @instance
 * @function post
 * @param   {Server~methodOpts} post - if string, the URL to handle.
 *                                 if options, the URL to handle, at minimum.
 * @returns {Route}                the newly created route.
 */
Server.prototype.post = serverMethodFactory('POST');

/**
 * Mounts a chain on the given path against this HTTP verb
 *
 * @public
 * @memberof Server
 * @instance
 * @function put
 * @param   {Server~methodOpts} put - if string, the URL to handle.
 *                                 if options, the URL to handle, at minimum.
 * @returns {Route}                the newly created route.
 */
Server.prototype.put = serverMethodFactory('PUT');

/**
 * Mounts a chain on the given path against this HTTP verb
 *
 * @public
 * @memberof Server
 * @instance
 * @function patch
 * @param   {Server~methodOpts} patch - if string, the URL to handle.
 *                                 if options, the URL to handle, at minimum.
 * @returns {Route}                the newly created route.
 */
Server.prototype.patch = serverMethodFactory('PATCH');

/**
 * Mounts a chain on the given path against this HTTP verb
 *
 * @public
 * @memberof Server
 * @instance
 * @function del
 * @param   {Server~methodOpts} opts - if string, the URL to handle.
 *                                 if options, the URL to handle, at minimum.
 * @returns {Route}                the newly created route.
 */
Server.prototype.del = serverMethodFactory('DELETE');

/**
 * Mounts a chain on the given path against this HTTP verb
 *
 * @public
 * @memberof Server
 * @instance
 * @function opts
 * @param   {Server~methodOpts} opts - if string, the URL to handle.
 *                                 if options, the URL to handle, at minimum.
 * @returns {Route}                the newly created route.
 */
Server.prototype.opts = serverMethodFactory('OPTIONS');

///---  Request lifecycle and middleware methods

// eslint-disable-next-line jsdoc/check-param-names
/**
 * Gives you hooks to run _before_ any routes are located.  This gives you
 * a chance to intercept the request and change headers, etc., that routing
 * depends on.  Note that req.params will _not_ be set yet.
 *
 * @public
 * @memberof Server
 * @instance
 * @function pre
 * @param {...Function|Array} handler - Allows you to add handlers that
 * run for all routes. *before* routing occurs.
 * This gives you a hook to change request headers and the like if you need to.
 * Note that `req.params` will be undefined, as that's filled in *after*
 * routing.
 * Takes a function, or an array of functions.
 * variable number of nested arrays of handler functions
 * @returns {Object} returns self
 * @example
 * server.pre(function(req, res, next) {
 *   req.headers.accept = 'application/json';
 *   return next();
 * });
 * @example
 * <caption>For example, `pre()` can be used to deduplicate slashes in
 * URLs</caption>
 * server.pre(restify.pre.dedupeSlashes());
 */
Server.prototype.pre = function pre() {
    var self = this;
    var handlers = Array.prototype.slice.call(arguments);

    argumentsToChain(handlers).forEach(function forEach(handler) {
        handler._name = handler.name || 'pre-' + self.preChain.count();
        self.preChain.add(handler);
    });

    return this;
};

// eslint-disable-next-line jsdoc/check-param-names
/**
 * Allows you to add in handlers that run for all routes. Note that handlers
 * added
 * via `use()` will run only after the router has found a matching route. If no
 * match is found, these handlers will never run. Takes a function, or an array
 * of functions.
 *
 * You can pass in any combination of functions or array of functions.
 *
 * @public
 * @memberof Server
 * @instance
 * @function use
 * @param {...Function|Array} handler - A variable number of handler functions
 * * and/or a
 * variable number of nested arrays of handler functions
 * @returns {Object} returns self
 */
Server.prototype.use = function use() {
    var self = this;
    var handlers = Array.prototype.slice.call(arguments);

    argumentsToChain(handlers).forEach(function forEach(handler) {
        handler._name = handler.name || 'use-' + self.useChain.count();
        self.useChain.add(handler);
    });

    return this;
};

/**
 * Minimal port of the functionality offered by Express.js Route Param
 * Pre-conditions
 *
 * This basically piggy-backs on the `server.use` method. It attaches a
 * new middleware function that only fires if the specified parameter exists
 * in req.params
 *
 * Exposes an API:
 *   server.param("user", function (req, res, next) {
 *     // load the user's information here, always making sure to call next()
 *   });
 *
 * @see http://expressjs.com/guide.html#route-param%20pre-conditions
 * @public
 * @memberof Server
 * @instance
 * @function param
 * @param    {String}   name - The name of the URL param to respond to
 * @param    {Function} fn -   The middleware function to execute
 * @returns  {Object}        returns self
 */
Server.prototype.param = function param(name, fn) {
    this.use(function _param(req, res, next) {
        if (req.params && req.params.hasOwnProperty(name)) {
            fn.call(this, req, res, next, req.params[name], name);
        } else {
            next();
        }
    });

    return this;
};

/**
 * Removes a route from the server.
 * You pass in the route 'blob' you got from a mount call.
 *
 * @public
 * @memberof Server
 * @instance
 * @function rm
 * @throws   {TypeError} on bad input.
 * @param    {String}    routeName - the route name.
 * @returns  {Boolean}   true if route was removed, false if not.
 */
Server.prototype.rm = function rm(routeName) {
    var route = this.router.unmount(routeName);
    return !!route;
};

///--- Info and debug methods

/**
 * Returns the server address.
 * Wraps node's
 * [address()](http://nodejs.org/docs/latest/api/net.html#net_server_address).
 *
 * @public
 * @memberof Server
 * @instance
 * @function address
 * @returns  {Object} Address of server
 * @example
 * server.address()
 * @example
 * <caption>Output:</caption>
 * { address: '::', family: 'IPv6', port: 8080 }
 */
Server.prototype.address = function address() {
    return this.server.address();
};

/**
 * Returns the number of inflight requests currently being handled by the server
 *
 * @public
 * @memberof Server
 * @instance
 * @function inflightRequests
 * @returns  {number} number of inflight requests
 */
Server.prototype.inflightRequests = function inflightRequests() {
    var self = this;
    return self._inflightRequests;
};

/**
 * Return debug information about the server.
 *
 * @public
 * @memberof Server
 * @instance
 * @function debugInfo
 * @returns {Object} debug info
 * @example
 * server.getDebugInfo()
 * @example
 * <caption>Output:</caption>
 * {
 *   routes: [
 *     {
 *       name: 'get',
 *       method: 'get',
 *       input: '/',
 *       compiledRegex: /^[\/]*$/,
 *       compiledUrlParams: null,
 *       handlers: [Array]
 *      }
 *   ],
 *   server: {
 *     formatters: {
 *       'application/javascript': [Function: formatJSONP],
 *       'application/json': [Function: formatJSON],
 *       'text/plain': [Function: formatText],
 *       'application/octet-stream': [Function: formatBinary]
 *     },
 *     address: '::',
 *     port: 8080,
 *     inflightRequests: 0,
 *     pre: [],
 *     use: [ 'parseQueryString', '_jsonp' ],
 *     after: []
 *   }
 * }
 */
Server.prototype.getDebugInfo = function getDebugInfo() {
    var self = this;

    // map an array of function to an array of function names
    var funcNameMapper = function funcNameMapper(handler) {
        if (handler.name === '') {
            return 'anonymous';
        } else {
            return handler.name;
        }
    };

    if (!self._debugInfo) {
        var addressInfo = self.server.address();

        // output the actual routes registered with restify
        var routeInfo = self.router.getDebugInfo();

        var preHandlers = self.preChain.getHandlers().map(funcNameMapper);
        var useHandlers = self.useChain.getHandlers().map(funcNameMapper);

        // get each route's handler chain
        var routes = _.map(routeInfo, function mapValues(route) {
            route.handlers = Array.prototype.concat.call(
                // TODO: should it contain use handlers?
                useHandlers,
                route.handlers.map(funcNameMapper)
            );
            return route;
        });

        self._debugInfo = {
            routes: routes,
            server: {
                formatters: self.formatters,
                // if server is not yet listening, addressInfo may be null
                address: addressInfo && addressInfo.address,
                port: addressInfo && addressInfo.port,
                inflightRequests: self.inflightRequests(),
                pre: preHandlers,
                use: useHandlers,
                after: self.listeners('after').map(funcNameMapper)
            }
        };
    }

    return self._debugInfo;
};

/**
 * toString() the server for easy reading/output.
 *
 * @public
 * @memberof Server
 * @instance
 * @function toString
 * @returns  {String} stringified server
 * @example
 * server.toString()
 * @example
 * <caption>Output:</caption>
 *	Accepts: application/json, text/plain, application/octet-stream,
 * application/javascript
 *	Name: restify
 *	Pre: []
 *	Router: RestifyRouter:
 *		DELETE: []
 *		GET: [get]
 *		HEAD: []
 *		OPTIONS: []
 *		PATCH: []
 *		POST: []
 *		PUT: []
 *
 *	Routes:
 *		get: [parseQueryString, _jsonp, function]
 *	Secure: false
 *	Url: http://[::]:8080
 *	Version:
 */
Server.prototype.toString = function toString() {
    var LINE_FMT = '\t%s: %s\n';
    var SUB_LINE_FMT = '\t\t%s: %s\n';
    var str = '';

    function handlersToString(arr) {
        var s =
            '[' +
            arr
                .map(function map(b) {
                    return b.name || 'function';
                })
                .join(', ') +
            ']';

        return s;
    }

    str += sprintf(LINE_FMT, 'Accepts', this.acceptable.join(', '));
    str += sprintf(LINE_FMT, 'Name', this.name);
    str += sprintf(
        LINE_FMT,
        'Pre',
        handlersToString(this.preChain.getHandlers())
    );
    str += sprintf(LINE_FMT, 'Router', '');
    this.router
        .toString()
        .split('\n')
        .forEach(function forEach(line) {
            str += sprintf('\t\t%s\n', line);
        });
    str += sprintf(LINE_FMT, 'Routes', '');
    _.forEach(this.router.getRoutes(), function forEach(route, routeName) {
        var handlers = handlersToString(route.chain.getHandlers());
        str += sprintf(SUB_LINE_FMT, routeName, handlers);
    });
    str += sprintf(LINE_FMT, 'Secure', this.secure);
    str += sprintf(LINE_FMT, 'Url', this.url);

    return str;
};

///--- Private methods

// Lifecycle:
//
// 1. _onRequest (handle new request, setup request and triggers pre)
//   2. _runPre
//     3. _afterPre (runs after pre handlers are finisehd, triggers route)
//       4. _runRoute (route lookup)
//         5. _runUse (runs use handlers, if route exists)
//            6. Runs route handlers
//              7. _afterRoute (runs after route handlers are finised,
//                          triggers use)
// 8. _finishReqResCycle (on response "finish" and "error" events)
//
// Events:
// e.1 after (triggered when response is flushed)
//
// Errors:
// e.1 _onHandlerError (runs when next was called with an Error)
//   e.2 _routeErrorResponse
// e.1 _onHandlerError (when, next('string') called, trigger route by name)
//  e.2 _afterRoute

/**
 * Setup request and calls _onRequest to run middlewares and call router
 *
 * @private
 * @memberof Server
 * @instance
 * @function _onRequest
 * @param    {Object}    req - the request object
 * @param    {Object}    res - the response object
 * @returns  {undefined} no return value
 * @fires Request,Response#request
 */
Server.prototype._onRequest = function _onRequest(req, res) {
    var self = this;

    this.emit('request', req, res);

    // Skip Socket.io endpoints
    if (this.socketio && /^\/socket\.io.*/.test(req.url)) {
        return;
    }

    // Decorate req and res objects
    self._setupRequest(req, res);

    // increment number of requests
    self._inflightRequests++;

    // Run in domain to catch async errors
    // It has significant negative performance impact
    // Warning: this feature depends on the deprecated domains module
    if (self.handleUncaughtExceptions) {
        var handlerDomain = domain.create();
        handlerDomain.add(req);
        handlerDomain.add(res);
        handlerDomain.on('error', function onError(err) {
            self._onHandlerError(err, req, res);
        });
        handlerDomain.run(function run() {
            self._runPre(req, res);
        });
    } else {
        self._runPre(req, res);
    }
};

/**
 * Run pre handlers
 *
 * @private
 * @memberof Server
 * @instance
 * @function _runPre
 * @param    {Object}    req - the request object
 * @param    {Object}    res - the response object
 * @returns  {undefined} no return value
 * @fires Request,Response#request
 */
Server.prototype._runPre = function _runPre(req, res) {
    var self = this;

    // emit 'pre' event before we run the pre handlers
    self.emit('pre', req, res);

    // Run "pre"
    req._currentHandler = 'pre';
    req._timePreStart = process.hrtime();

    self.preChain.run(req, res, function preChainDone(err) {
        // Execution time of a handler with error can be significantly lower
        req._timePreEnd = process.hrtime();
        self._afterPre(err, req, res);
    });
};

/**
 * After pre handlers finished
 *
 * @private
 * @memberof Server
 * @instance
 * @function _afterPre
 * @param  {Error|false|undefined} err - pre handler error
 * @param  {Request} req - request
 * @param  {Response} res - response
 * @returns {undefined} no return value
 */
Server.prototype._afterPre = function _afterPre(err, req, res) {
    var self = this;

    // Handle error
    if (err) {
        self._onHandlerError(err, req, res);
        self._finishReqResCycle(req, res, err);
        return;
    }

    // Stop
    if (err === false) {
        self._onHandlerStop(req, res);
        return;
    }

    self._runRoute(req, res);
};

/**
 * Find route and run handlers
 *
 * @private
 * @memberof Server
 * @instance
 * @function _runRoute
 * @param    {Object}    req - the request object
 * @param    {Object}    res - the response object
 * @returns  {undefined} no return value
 * @fires Request,Response#request
 */
Server.prototype._runRoute = function _runRoute(req, res) {
    var self = this;

    var routeHandler = self.router.lookup(req, res);

    if (!routeHandler) {
        self.router.defaultRoute(req, res, function afterRouter(err) {
            self._afterRoute(err, req, res);
        });
        return;
    }

    // Emit routed
    self.emit('routed', req, res, req.route);

    self._runUse(req, res, function afterUse() {
        // DTrace
        if (self.dtrace) {
            dtrace._rstfy_probes['route-start'].fire(function fire() {
                return [
                    self.name,
                    req.route.name,
                    req._dtraceId,
                    req.method,
                    req.href(),
                    req.headers
                ];
            });
        }

        req._timeRouteStart = process.hrtime();
        routeHandler(req, res, function afterRouter(err) {
            // Execution time of a handler with error can be significantly lower
            req._timeRouteEnd = process.hrtime();

            // DTrace
            if (self.dtrace) {
                dtrace._rstfy_probes['route-done'].fire(function fire() {
                    return [
                        self.name,
                        req.route.name,
                        req._dtraceId,
                        res.statusCode || 200,
                        res.headers
                    ];
                });
            }

            self._afterRoute(err, req, res);
        });
    });
};

/**
 * After use handlers finished
 *
 * @private
 * @memberof Server
 * @instance
 * @function _afterRoute
 * @param  {Error|false|undefined} err - use handler error
 * @param  {Request} req - request
 * @param  {Response} res - response
 * @returns {undefined} no return value
 */
Server.prototype._afterRoute = function _afterRoute(err, req, res) {
    var self = this;

    res._handlersFinished = true;

    // Handle error
    if (err) {
        self._onHandlerError(err, req, res);
        self._finishReqResCycle(req, res, err);
        return;
    }

    // Trigger finish
    self._finishReqResCycle(req, res, err);
};

/**
 * Run use handlers
 *
 * @private
 * @memberof Server
 * @instance
 * @function _runUse
 * @param    {Object}    req - the request object
 * @param    {Object}    res - the response object
 * @param    {Function}  next - next
 * @returns  {undefined} no return value
 * @fires Request,Response#request
 */
Server.prototype._runUse = function _runUse(req, res, next) {
    var self = this;

    // Run "use"
    req._currentHandler = 'use';
    req._timeUseStart = process.hrtime();

    self.useChain.run(req, res, function useChainDone(err) {
        // Execution time of a handler with error can be significantly lower
        req._timeUseEnd = process.hrtime();
        self._afterUse(err, req, res, next);
    });
};

/**
 * After use handlers finished
 *
 * @private
 * @memberof Server
 * @instance
 * @function _afterUse
 * @param  {Error|false|undefined} err - use handler error
 * @param  {Request} req - request
 * @param  {Response} res - response
 * @param  {Function}  next - next
 * @returns {undefined} no return value
 */
Server.prototype._afterUse = function _afterUse(err, req, res, next) {
    var self = this;

    // Handle error
    if (err) {
        self._onHandlerError(err, req, res);
        self._finishReqResCycle(req, res, err);
        return;
    }

    // Stop
    if (err === false) {
        self._onHandlerStop(req, res);
        return;
    }

    next();
};

/**
 * Runs after next(false) is called
 *
 * @private
 * @memberof Server
 * @instance
 * @function _onHandlerStop
 * @param  {Request} req - request
 * @param  {Response} res - response
 * @returns {undefined} no return value
 */
Server.prototype._onHandlerStop = function _onHandlerStop(req, res) {
    res._handlersFinished = true;
    this._finishReqResCycle(req, res);
};

/**
 * After route handlers finished
 * NOTE: only called when last handler calls next([err])
 *
 * @private
 * @memberof Server
 * @instance
 * @function _onHandlerError
 * @param  {Error|String|undefined} err - router handler error or route name
 * @param  {Request} req - request
 * @param  {Response} res - response
 * @returns {undefined} no return value
 */
Server.prototype._onHandlerError = function _onHandlerError(err, req, res) {
    var self = this;

    // Call route by name
    if (_.isString(err)) {
        var routeName = err;
        var routeHandler = self.router.lookupByName(routeName, req, res);

        // Cannot find route by name, called when next('route-name') doesn't
        // find any route, it's a 5xx error as it's a programatic error
        if (!routeHandler) {
            var routeByNameErr = new errors.RouteMissingError(
                "Route by name doesn't exist"
            );
            routeByNameErr.code = 'ENOEXIST';
            self._afterRoute(routeByNameErr, req, res);
            return;
        }
        routeHandler(req, res, function afterRouter(routeErr) {
            self._afterRoute(routeErr, req, res);
        });
        return;
    }

    res._handlersFinished = true;

    // Preserve handler err for finish event
    res.err = res.err || err;

    // Error happened in router handlers
    self._routeErrorResponse(req, res, err);
};

/**
 * Set up the request before routing and execution of handler chain functions.
 *
 * @private
 * @memberof Server
 * @instance
 * @function _setupRequest
 * @param    {Object}    req - the request object
 * @param    {Object}    res - the response object
 * @returns  {undefined} no return value
 */
Server.prototype._setupRequest = function _setupRequest(req, res) {
    var self = this;

    // Extend request
    req._dtraceId = dtrace.nextId();
    req.log = res.log = self.log;
    req._date = new Date();
    req._timeStart = process.hrtime();
    req.serverName = self.name;
    req.params = {};
    req.timers = [];
    req.dtrace = self.dtrace;

    // Extend response
    res.acceptable = self.acceptable;
    res.formatters = self.formatters;
    res.req = req;
    res.serverName = self.name;
    res._handlersFinished = false;
    res._flushed = false;

    // set header only if name isn't empty string
    if (self.name !== '') {
        res.setHeader('Server', self.name);
    }

    // Request lifecycle events
    // attach a listener for 'close' events, this will let us set
    // a flag so that we can stop processing the request if the client closes
    // the connection (or we lose the connection).
    // we consider a closed request as flushed from metrics point of view
    function onClose() {
        res._flushed = true;
        req._timeFlushed = process.hrtime();

        res.removeListener('finish', onFinish);
        res.removeListener('error', onError);
        req._connectionState = 'close';

        // Request was aborted or closed. Override the status code
        res.statusCode = 444;

        self._finishReqResCycle(req, res, new errors.RequestCloseError());
    }
    req.once('close', onClose);

    // Response lifecycle events
    function onFinish() {
        var processHrTime = process.hrtime();

        res._flushed = true;
        req._timeFlushed = processHrTime;

        // Response may get flushed before handler callback is triggered
        req._timeFlushed = processHrTime;
        req._timePreEnd = req._timePreEnd || processHrTime;
        req._timeUseEnd = req._timeUseEnd || processHrTime;
        req._timeRouteEnd = req._timeRouteEnd || processHrTime;

        req.removeListener('close', onClose);
        res.removeListener('error', onError);

        // Do not trigger false
        self._finishReqResCycle(req, res);
    }
    function onError(err) {
        res._flushed = true;
        req._timeFlushed = process.hrtime();

        req.removeListener('close', onClose);
        res.removeListener('finish', onFinish);
        self._finishReqResCycle(req, res, err);
    }
    res.once('finish', onFinish);
    res.once('error', onError);

    // attach a listener for the response's 'redirect' event
    res.on('redirect', function onRedirect(redirectLocation) {
        self.emit('redirect', redirectLocation);
    });
};

/**
 * Maintaining the end of the request-response cycle:
 *  - emitting after event
 *  - updating inflight requests metrics
 * Check if the response is finished, and if not, wait for it before firing the
 * response object.
 *
 * @private
 * @memberof Server
 * @instance
 * @function _finishReqResCycle
 * @param {Object} req - the request object
 * @param {Object} res - the response object
 * @param {Object} [err] - a possible error as a result of failed route matching
 * or failed execution of the handler array.
 * @returns {undefined} no return value
 */
Server.prototype._finishReqResCycle = function _finishReqResCycle(
    req,
    res,
    err
) {
    var self = this;
    var route = req.route; // can be undefined when 404 or error

    if (res._finished) {
        return;
    }

    if (res._flushed && res._handlersFinished) {
        // decrement number of requests
        self._inflightRequests--;
        res._finished = true;
        req._timeFinished = process.hrtime();

        // after event has signature of function(req, res, route, err) {...}
        self.emit('after', req, res, route, err || res.err);
    } else {
        // preserve error for actual finish
        res.err = err;
    }
};

/**
 * Helper function to, when on router error, emit error events and then
 * flush the err.
 *
 * @private
 * @memberof Server
 * @instance
 * @function _routeErrorResponse
 * @param    {Request}     req -    the request object
 * @param    {Response}    res -    the response object
 * @param    {Error}       err -    error
 * @returns  {undefined} no return value
 */
Server.prototype._routeErrorResponse = function _routeErrorResponse(
    req,
    res,
    err
) {
    var self = this;

    // if (self.handleUncaughtExceptions) {
    if (self.listenerCount('uncaughtException') > 1) {
        self.emit('uncaughtException', req, res, req.route, err);
        return;
    }

    self._emitErrorEvents(req, res, null, err, function emitError(emitErr) {
        // Prevent double handling
        if (res._sent) {
            return;
        }

        // Expose only knonw errors
        if (_.isNumber(err.statusCode)) {
            res.send(err);
            return;
        }

        res.send(new errors.InternalError(emitErr || err));
    });
};

/**
 * Emit error events when errors are encountered either while attempting to
 * route the request (via router) or while executing the handler chain.
 *
 * @private
 * @memberof Server
 * @instance
 * @function _emitErrorEvents
 * @param    {Object} req -    the request object
 * @param    {Object} res -    the response object
 * @param    {Object} route -  the current route, if applicable
 * @param    {Object} err -    an error object
 * @param    {Object} cb -     callback function
 * @returns  {undefined} no return value
 * @fires Error#restifyError
 */
Server.prototype._emitErrorEvents = function _emitErrorEvents(
    req,
    res,
    route,
    err,
    cb
) {
    var self = this;
    var errName = errEvtNameFromError(err);

    req.log.trace(
        {
            err: err,
            errName: errName
        },
        'entering emitErrorEvents',
        err.name
    );

    var errEvtNames = [];

    // if we have listeners for the specific error, fire those first.
    if (self.listeners(errName).length > 0) {
        errEvtNames.push(errName);
    }

    // or if we have a generic error listener. always fire generic error event
    // listener afterwards.
    if (self.listeners('restifyError').length > 0) {
        errEvtNames.push('restifyError');
    }

    // kick off the async listeners
    return vasync.forEachPipeline(
        {
            inputs: errEvtNames,
            func: function emitError(errEvtName, vasyncCb) {
                self.emit(errEvtName, req, res, err, function emitErrDone() {
                    // the error listener may return arbitrary objects, throw
                    // them away and continue on. don't want vasync to take
                    // that error and stop, we want to emit every single event.
                    return vasyncCb();
                });
            }
        },
        // eslint-disable-next-line handle-callback-err
        function onResult(nullErr, results) {
            // vasync will never return error here. callback with the original
            // error to pass it on.
            return cb(err);
        }
    );
};

///--- Helpers

/**
 * Verify and flatten a nested array of request handlers.
 *
 * @private
 * @function argumentsToChain
 * @throws   {TypeError}
 * @param    {Function[]} handlers - pass through of funcs from server.[method]
 * @returns  {Array} request handlers
 */
function argumentsToChain(handlers) {
    assert.array(handlers, 'handlers');

    var chain = [];

    // A recursive function for unwinding a nested array of handlers into a
    // single chain.
    function process(array) {
        for (var i = 0; i < array.length; i++) {
            if (Array.isArray(array[i])) {
                // Recursively call on nested arrays
                process(array[i]);
                continue;
            }
            // If an element of the array isn't an array, ensure it is a
            // handler function and then push it onto the chain of handlers
            assert.func(array[i], 'handler');
            chain.push(array[i]);
        }

        return chain;
    }

    // Return the chain, note that if `handlers` is an empty array, this will
    // return an empty array.
    return process(handlers);
}

/**
 * merge optional formatters with the default formatters to create a single
 * formatters object. the passed in optional formatters object looks like:
 * formatters: {
 *   'application/foo': function formatFoo(req, res, body) {...}
 * }
 * @private
 * @function mergeFormatters
 * @param    {Object} fmt user specified formatters object
 * @returns  {Object}
 */

function mergeFormatters(fmt) {
    var arr = [];
    var obj = {};

    function addFormatter(src, k) {
        assert.func(src[k], 'formatter');

        var q = 1.0; // RFC 2616 sec14 - The default value is q=1
        var t = k;

        if (k.indexOf(';') !== -1) {
            var tmp = k.split(/\s*;\s*/);
            t = tmp[0];

            if (tmp[1].indexOf('q=') !== -1) {
                q = parseFloat(tmp[1].split('=')[1]);
            }
        }

        if (k.indexOf('/') === -1) {
            k = mime.lookup(k);
        }

        obj[t] = src[k];
        arr.push({
            q: q,
            t: t
        });
    }

    Object.keys(formatters).forEach(addFormatter.bind(this, formatters));
    Object.keys(fmt || {}).forEach(addFormatter.bind(this, fmt || {}));

    arr = arr
        .sort(function sort(a, b) {
            return b.q - a.q;
        })
        .map(function map(a) {
            return a.t;
        });

    return {
        formatters: obj,
        acceptable: arr
    };
}

/**
 * Map an Error's .name property into the actual event name that is emitted
 * by the restify server object.
 *
 * @function
 * @private errEvtNameFromError
 * @param {Object} err - an error object
 * @returns {String} an event name to emit
 */
function errEvtNameFromError(err) {
    if (err.name === 'ResourceNotFoundError') {
        // remap the name for router errors
        return 'NotFound';
    } else if (err.name === 'InvalidVersionError') {
        // remap the name for router errors
        return 'VersionNotAllowed';
    } else {
        return err.name.replace(/Error$/, '');
    }
}

/**
 * Mounts a chain on the given path against this HTTP verb
 *
 * @private
 * @function serverMethodFactory
 * @param {String} method - name of the HTTP method
 * @returns {Function} factory
 */
function serverMethodFactory(method) {
    return function serverMethod(opts) {
        if (opts instanceof RegExp || typeof opts === 'string') {
            opts = {
                path: opts
            };
        } else if (typeof opts === 'object') {
            opts = shallowCopy(opts);
        } else {
            throw new TypeError('path (string) required');
        }

        if (arguments.length < 2) {
            throw new TypeError('handler (function) required');
        }

        opts.method = method;
        opts.path = opts.path || opts.url;

        // We accept both a variable number of handler functions, a
        // variable number of nested arrays of handler functions, or a mix
        // of both
        var handlers = Array.prototype.slice.call(arguments, 1);
        var chain = argumentsToChain(handlers);
        var route = this.router.mount(opts, chain);

        return route.name;
    };
}
