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

'use strict';

var http = require('http');
var sprintf = require('util').format;
var url = require('url');

var assert = require('assert-plus');
var mime = require('mime');
var errors = require('restify-errors');

var httpDate = require('./http_date');
var utils = require('./utils');

///--- Globals

var InternalServerError = errors.InternalServerError;

/**
 * @private
 * Headers that cannot be multi-values.
 * @see #779, multiple set-cookie values are allowed only as multiple headers.
 * @see #986, multiple content-type values / headers disallowed.
 */
var HEADER_ARRAY_BLACKLIST = {
    'content-type': true
};

///--- API

/**
 * Patch Response object and extends with extra functionalities
 *
 * @private
 * @function patch
 * @param    {http.ServerResponse|http2.Http2ServerResponse} Response -
 *                                                           Server Response
 * @returns  {undefined} No return value
 */
function patch(Response) {
    assert.func(Response, 'Response');

    /**
     * Wraps all of the node
     * [http.ServerResponse](https://nodejs.org/docs/latest/api/http.html)
     * APIs, events and properties, plus the following.
     * @class Response
     * @extends http.ServerResponse
     */

    /**
     * Sets the `cache-control` header.
     *
     * @public
     * @memberof Response
     * @instance
     * @function cache
     * @param    {String} [type="public"] - value of the header
     *                                    (`"public"` or `"private"`)
     * @param    {Object} [options] - an options object
     * @param    {Number} options.maxAge - max-age in seconds
     * @returns  {String}         the value set to the header
     */
    Response.prototype.cache = function cache(type, options) {
        if (typeof type !== 'string') {
            options = type;
            type = 'public';
        }

        if (options && options.maxAge !== undefined) {
            assert.number(options.maxAge, 'options.maxAge');
            type += ', max-age=' + options.maxAge;
        }

        return this.setHeader('Cache-Control', type);
    };

    /**
     * Turns off all cache related headers.
     *
     * @public
     * @memberof Response
     * @instance
     * @function noCache
     * @returns  {Response} self, the response object
     */
    Response.prototype.noCache = function noCache() {
        // HTTP 1.1
        this.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');

        // HTTP 1.0
        this.setHeader('Pragma', 'no-cache');

        // Proxies
        this.setHeader('Expires', '0');

        return this;
    };

    /**
     * Appends the provided character set to the response's `Content-Type`.
     *
     * @public
     * @memberof Response
     * @instance
     * @function charSet
     * @param    {String} type - char-set value
     * @returns  {Response} self, the response object
     * @example
     * res.charSet('utf-8');
     */
    Response.prototype.charSet = function charSet(type) {
        assert.string(type, 'charset');

        this._charSet = type;

        return this;
    };

    /**
     * Retrieves a header off the response.
     *
     * @private
     * @memberof Response
     * @instance
     * @function get
     * @param    {Object} name - the header name
     * @returns  {String} header value
     */
    Response.prototype.get = function get(name) {
        assert.string(name, 'name');

        return this.getHeader(name);
    };

    // If getHeaders is not provided by the Node platform, monkey patch our own.
    // This is needed since versions of Node <7 did not come with a getHeaders.
    // For more see GH-1408
    if (typeof Response.prototype.getHeaders !== 'function') {
        /**
         * Retrieves all headers off the response.
         *
         * @private
         * @memberof Response
         * @instance
         * @function getHeaders
         * @returns  {Object} headers
         */
        Response.prototype.getHeaders = function getHeaders() {
            return this._headers || {};
        };
    }

    /**
     * Sets headers on the response.
     *
     * @public
     * @memberof Response
     * @instance
     * @function header
     * @param    {String} key - the name of the header
     * @param    {String} value - the value of the header
     * @returns  {Object} the retrieved value or the value that was set
     * @example
     * <caption>
     * If only key is specified, return the value of the header.
     * If both key and value are specified, set the response header.
     * </caption>
     * res.header('Content-Length');
     * // => undefined
     *
     * res.header('Content-Length', 123);
     * // => 123
     *
     * res.header('Content-Length');
     * // => 123
     *
     * res.header('foo', new Date());
     * // => Fri, 03 Feb 2012 20:09:58 GMT
     * @example
     * <caption>
     * `header()` can also be used to automatically chain header values
     * when applicable:
     * </caption>
     * res.header('x-foo', 'a');
     * res.header('x-foo', 'b');
     * // => { 'x-foo': ['a', 'b'] }
     * @example
     * <caption>
     * Note that certain headers like `content-type`
     * do not support multiple values, so calling `header()`
     * twice for those headers will
     * overwrite the existing value.
     * </caption>
     */
    Response.prototype.header = function header(key, value) {
        assert.string(key, 'name');

        if (value === undefined) {
            return this.getHeader(key);
        }

        if (value instanceof Date) {
            value = httpDate(value);
        } else if (arguments.length > 2) {
            // Support res.header('foo', 'bar %s', 'baz');
            var arg = Array.prototype.slice.call(arguments).slice(2);
            value = sprintf(value, arg);
        }

        var current = this.getHeader(key);

        // Check the header blacklist before changing a header to an array
        var keyLc = key.toLowerCase();

        if (current && !(keyLc in HEADER_ARRAY_BLACKLIST)) {
            if (Array.isArray(current)) {
                current.push(value);
                value = current;
            } else {
                value = [current, value];
            }
        }

        this.setHeader(key, value);
        return value;
    };

    /**
     * Syntatic sugar for:
     * ```js
     * res.contentType = 'json';
     * res.send({hello: 'world'});
     * ```
     *
     * @public
     * @memberof Response
     * @instance
     * @function json
     * @param    {Number} [code] -    http status code
     * @param    {Object} [body] -    value to json.stringify
     * @param    {Object} [headers] - headers to set on the response
     * @returns  {Object} the response object
     * @example
     * res.header('content-type', 'json');
     * res.send({hello: 'world'});
     */
    Response.prototype.json = function json(code, body, headers) {
        this.setHeader('Content-Type', 'application/json');
        return this.send(code, body, headers);
    };

    /**
     * Sets the link header.
     *
     * @public
     * @memberof Response
     * @instance
     * @function link
     * @param    {String} key -  the link key
     * @param    {String} value - the link value
     * @returns  {String}     the header value set to res
     */
    Response.prototype.link = function link(key, value) {
        assert.string(key, 'key');
        assert.string(value, 'value');

        var _link = sprintf('<%s>; rel="%s"', key, value);
        return this.header('Link', _link);
    };

    /**
     * Sends the response object. pass through to internal `__send` that uses a
     * formatter based on the `content-type` header.
     *
     * @public
     * @memberof Response
     * @instance
     * @function send
     * @param    {Number} [code] - http status code
     * @param    {Object | Buffer | Error} [body] - the content to send
     * @param    {Object} [headers] - any add'l headers to set
     * @returns  {Object} the response object
     * @example
     * <caption>
     * You can use send() to wrap up all the usual writeHead(), write(), end()
     * calls on the HTTP API of node.
     * You can pass send either a `code` and `body`, or just a body. body can be
     * an `Object`, a `Buffer`, or an `Error`.
     * When you call `send()`, restify figures out how to format the response
     * based on the `content-type`.
     * </caption>
     * res.send({hello: 'world'});
     * res.send(201, {hello: 'world'});
     * res.send(new BadRequestError('meh'));
     */
    Response.prototype.send = function send(code, body, headers) {
        var self = this;
        var sendArgs;

        if (typeof code === 'number') {
            sendArgs = {
                code: code,
                body: body,
                headers: headers
            };
        } else {
            sendArgs = {
                body: code,
                headers: body
            };
        }

        sendArgs.format = true;
        return self.__send(sendArgs);
    };

    /**
     * Like `res.send()`, but skips formatting. This can be useful when the
     * payload has already been preformatted.
     * Sends the response object. pass through to internal `__send` that skips
     * formatters entirely and sends the content as is.
     *
     * @public
     * @memberof Response
     * @instance
     * @function sendRaw
     * @param    {Number} [code] - http status code
     * @param    {Object | Buffer | Error} [body] - the content to send
     * @param    {Object} [headers] - any add'l headers to set
     * @returns  {Object} the response object
     */
    Response.prototype.sendRaw = function sendRaw(code, body, headers) {
        var self = this;
        var sendArgs;

        if (typeof code === 'number') {
            sendArgs = {
                code: code,
                body: body,
                headers: headers
            };
        } else {
            sendArgs = {
                body: code,
                headers: body
            };
        }

        assert.ok(
            typeof sendArgs.body === 'string' || Buffer.isBuffer(sendArgs.body),
            'res.sendRaw() accepts only strings or buffers'
        );

        sendArgs.format = false;
        return self.__send(sendArgs);
    };

    // eslint-disable-next-line jsdoc/check-param-names
    /**
     * Internal implementation of send. convenience method that handles:
     * writeHead(), write(), end().
     *
     * Both body and headers are optional, but you MUST provide body if you are
     * providing headers.
     *
     * @private
     * @param {Object} opts - an option sobject
     * @param {Object | Buffer | String | Error} opts.body - the content to send
     * @param {Boolean} opts.format - When false, skip formatting
     * @param {Number} [opts.code] - http status code
     * @param {Object} [opts.headers] - any add'l headers to set
     * @returns {Object} - returns the response object
     */
    Response.prototype.__send = function __send(opts) {
        var self = this;
        var isHead = self.req.method === 'HEAD';
        var log = self.log;
        var code = opts.code;
        var body = opts.body;
        var headers = opts.headers || {};

        self._sent = true;

        // Now lets try to derive values for optional arguments that we were not
        // provided, otherwise we choose sane defaults.

        // If the body is an error object and we were not given a status code,
        // try to derive it from the error object, otherwise default to 500
        if (!code && body instanceof Error) {
            code = body.statusCode || 500;
        }

        // Set sane defaults for optional arguments if they were not provided
        // and we failed to derive their values
        code = code || self.statusCode || 200;

        // Populate our response object with the derived arguments
        self.statusCode = code;
        self._body = body;
        Object.keys(headers).forEach(function forEach(k) {
            self.setHeader(k, headers[k]);
        });

        // If log level is set to trace, output our constructed response object
        if (log.trace()) {
            var _props = {
                code: self.statusCode,
                headers: self._headers
            };

            if (body instanceof Error) {
                _props.err = self._body;
            } else {
                _props.body = self._body;
            }
            log.trace(_props, 'response::send entered');
        }

        // 204 = No Content and 304 = Not Modified, we don't want to send the
        // body in these cases. HEAD never provides a body.
        if (isHead || code === 204 || code === 304) {
            return flush(self);
        }

        if (opts.format === true) {
            // if no body, then no need to format. if this was an error caught
            // by a domain, don't send the domain error either.
            if (body === undefined || (body instanceof Error && body.domain)) {
                return flush(self);
            }

            // At this point we know we have a body that needs to be formatted,
            // so lets derive the formatter based on the response object's
            // properties

            var formatter;
            var type = self.contentType || self.getHeader('Content-Type');

            // Set Content-Type to application/json when
            // res.send is called with an Object instead of calling res.json
            if (!type && typeof body === 'object' && !Buffer.isBuffer(body)) {
                type = 'application/json';
            }

            // Derive type if not provided by the user
            type = type || self.req.accepts(self.acceptable);

            // Check to see if we could find a content type to use for the
            // response.
            if (!type) {
                return formatterError(
                    self,
                    new errors.NotAcceptableError({
                        message:
                            'could not find suitable content-type to use ' +
                            'for the response'
                    })
                );
            }

            type = type.split(';')[0];

            if (!self.formatters[type] && type.indexOf('/') === -1) {
                type = mime.getType(type);
            }

            // If finding a formatter matching the negotiated content-type is
            // required, and we were unable to derive a valid type, default to
            // treating it as arbitrary binary data per RFC 2046 Section 4.5.1
            if (
                this._strictFormatters &&
                !self.formatters[type] &&
                self.acceptable.indexOf(type) === -1
            ) {
                type = 'application/octet-stream';
            }

            formatter = self.formatters[type] || self.formatters['*/*'];

            // If after the above attempts we were still unable to derive a
            // formatter, provide a meaningful error message
            if (this._strictFormatters && !formatter) {
                return formatterError(
                    self,
                    new errors.InternalServerError({
                        message:
                            'could not find formatter for response ' +
                            'content-type "' +
                            type +
                            '"'
                    })
                );
            }

            var formatterType = type;

            if (self._charSet) {
                type = type + '; charset=' + self._charSet;
            }

            // Update Content-Type header to the one originally set or to the
            // type inferred from the most relevant formatter found.
            self.setHeader('Content-Type', type);

            if (formatter) {
                // Finally, invoke the formatter and flush the request with it's
                // results

                var formattedBody;
                try {
                    formattedBody = formatter(self.req, self, body);
                } catch (e) {
                    if (
                        e instanceof errors.RestError ||
                        e instanceof errors.HttpError
                    ) {
                        var res = formatterError(
                            self,
                            e,
                            'error in formatter (' +
                                formatterType +
                                ') formatting response body'
                        );
                        return res;
                    }
                    throw e;
                }
                return flush(self, formattedBody);
            }
        }

        return flush(self, body);
    };

    /**
     * Sets multiple header(s) on the response.
     * Uses `header()` underneath the hood, enabling multi-value headers.
     *
     * @public
     * @memberof Response
     * @instance
     * @function set
     * @param    {String|Object} name - name of the header or
     *                                `Object` of headers
     * @param    {String} val - value of the header
     * @returns  {Object}      self, the response object
     * @example
     * res.header('x-foo', 'a');
     * res.set({
     *     'x-foo', 'b',
     *     'content-type': 'application/json'
     * });
     * // =>
     * // {
     * //    'x-foo': [ 'a', 'b' ],
     * //    'content-type': 'application/json'
     * // }
     */
    Response.prototype.set = function set(name, val) {
        var self = this;

        if (arguments.length === 2) {
            assert.string(
                name,
                'res.set(name, val) requires name to be a string'
            );
            this.header(name, val);
        } else {
            assert.object(
                name,
                'res.set(headers) requires headers to be an object'
            );
            Object.keys(name).forEach(function forEach(k) {
                self.set(k, name[k]);
            });
        }

        return this;
    };

    /**
     * Sets the http status code on the response.
     *
     * @public
     * @memberof Response
     * @instance
     * @function status
     * @param    {Number} code - http status code
     * @returns  {Number}        the status code passed in
     * @example
     * res.status(201);
     */
    Response.prototype.status = function status(code) {
        assert.number(code, 'code');

        this.statusCode = code;
        return code;
    };

    /**
     * toString() serialization.
     *
     * @private
     * @memberof Response
     * @instance
     * @function toString
     * @returns  {String} stringified response
     */
    Response.prototype.toString = function toString() {
        var headers = this.getHeaders();
        var headerString = '';
        var str;

        Object.keys(headers).forEach(function forEach(k) {
            headerString += k + ': ' + headers[k] + '\n';
        });
        str = sprintf(
            'HTTP/1.1 %s %s\n%s',
            this.statusCode,
            http.STATUS_CODES[this.statusCode],
            headerString
        );

        return str;
    };

    if (!Response.prototype.hasOwnProperty('_writeHead')) {
        Response.prototype._writeHead = Response.prototype.writeHead;
    }

    /**
     * Pass through to native response.writeHead()
     *
     * @private
     * @memberof Response
     * @instance
     * @function writeHead
     * @fires    header
     * @returns  {undefined} no return value
     */
    Response.prototype.writeHead = function restifyWriteHead() {
        this.emit('header');

        if (this.statusCode === 204 || this.statusCode === 304) {
            this.removeHeader('Content-Length');
            this.removeHeader('Content-MD5');
            this.removeHeader('Content-Type');
            this.removeHeader('Content-Encoding');
        }

        this._writeHead.apply(this, arguments);
    };

    /**
     * Redirect is sugar method for redirecting.
     * @public
     * @memberof Response
     * @instance
     * @param {Object} options url or an options object to configure a redirect
     * @param {Boolean} [options.secure] whether to redirect to http or https
     * @param {String} [options.hostname] redirect location's hostname
     * @param {String} [options.pathname] redirect location's pathname
     * @param {String} [options.port] redirect location's port number
     * @param {String} [options.query] redirect location's query string
     *                                 parameters
     * @param {Boolean} [options.overrideQuery] if true, `options.query`
     *                                          stomps over any existing query
     *                                          parameters on current URL.
     *                                          by default, will merge the two.
     * @param {Boolean} [options.permanent] if true, sets 301. defaults to 302.
     * @param {Function} next mandatory, to complete the response and trigger
     *                        audit logger.
     * @fires    redirect
     * @function redirect
     * @returns  {undefined}
     * @example
     * res.redirect({...}, next);
     * @example
     * <caption>
     * A convenience method for 301/302 redirects. Using this method will tell
     * restify to stop execution of your handler chain.
     * You can also use an options object. `next` is required.
     * </caption>
     * res.redirect({
     *   hostname: 'www.foo.com',
     *   pathname: '/bar',
     *   port: 80,                 // defaults to 80
     *   secure: true,             // sets https
     *   permanent: true,
     *   query: {
     *     a: 1
     *   }
     * }, next);  // => redirects to 301 https://www.foo.com/bar?a=1
     */

    /**
     * Redirect with code and url.
     * @memberof Response
     * @instance
     * @param {Number} code http redirect status code
     * @param {String} url redirect url
     * @param {Function} next mandatory, to complete the response and trigger
     *                        audit logger.
     * @fires    redirect
     * @function redirect
     * @returns  {undefined}
     * @example
     * res.redirect(301, 'www.foo.com', next);
     */

    /**
     * Redirect with url.
     * @public
     * @memberof Response
     * @instance
     * @param {String} url redirect url
     * @param {Function} next mandatory, to complete the response and trigger
     *                        audit logger.
     * @fires    redirect
     * @function redirect
     * @returns  {undefined}
     * @example
     * res.redirect('www.foo.com', next);
     * res.redirect('/foo', next);
     */
    Response.prototype.redirect = redirect;

    /**
     * @private
     * @param {*} arg1 - arg1
     * @param {*} arg2 - arg2
     * @param {*} arg3 - arg3
     * @fires    redirect
     * @function redirect
     * @returns  {undefined} no return value
     */
    function redirect(arg1, arg2, arg3) {
        var self = this;
        var statusCode = 302;
        var finalUri;
        var redirectLocation;
        var next;

        // 1) this is signature 1, where an explicit status code is passed in.
        //    MUST guard against null here, passing null is likely indicative
        //    of an attempt to call res.redirect(null, next);
        //    as a way to do a reload of the current page.
        if (arg1 && !isNaN(arg1)) {
            statusCode = arg1;
            finalUri = arg2;
            next = arg3;
        } else if (typeof arg1 === 'string') {
            // 2) this is signaure number 2
            // otherwise, it's a string, and use it directly
            finalUri = arg1;
            next = arg2;
        } else if (typeof arg1 === 'object') {
            // 3) signature number 3, using an options object.
            // set next, then go to work.
            next = arg2;

            var req = self.req;
            var opt = arg1 || {};
            var currentFullPath = req.href();
            var secure = opt.hasOwnProperty('secure')
                ? opt.secure
                : req.isSecure();

            // if hostname is passed in, use that as the base,
            // otherwise fall back on current url.
            var parsedUri = url.parse(opt.hostname || currentFullPath, true);

            // create the object we'll use to format for the final uri.
            // this object will eventually get passed to url.format().
            // can't use parsedUri to seed it, as it confuses the url module
            // with some existing parsed state. instead, we'll pick the things
            // we want and use that as a starting point.
            finalUri = {
                port: parsedUri.port,
                hostname: parsedUri.hostname,
                query: parsedUri.query,
                pathname: parsedUri.pathname
            };

            // start building url based on options.
            // start with the host
            if (opt.hostname) {
                finalUri.hostname = opt.hostname;
            }

            // then set protocol IFF hostname is set - otherwise we end up with
            // malformed URL.
            if (finalUri.hostname) {
                finalUri.protocol = secure === true ? 'https' : 'http';
            }

            // then set current path after the host
            if (opt.pathname) {
                finalUri.pathname = opt.pathname;
            }

            // then set port
            if (opt.port) {
                finalUri.port = opt.port;
            }

            // then add query params
            if (opt.query) {
                if (opt.overrideQuery === true) {
                    finalUri.query = opt.query;
                } else {
                    finalUri.query = utils.mergeQs(opt.query, finalUri.query);
                }
            }

            // change status code to 301 permanent if specified
            if (opt.permanent) {
                statusCode = 301;
            }
        }

        // if we're missing a next we should probably throw. if user wanted
        // to redirect but we were unable to do so, we should not continue
        // down the handler stack.
        assert.func(next, 'res.redirect() requires a next param');

        // if we are missing a finalized uri
        // by this point, pass an error to next.
        if (!finalUri) {
            return next(new InternalServerError('could not construct url'));
        }

        redirectLocation = url.format(finalUri);

        self.emit('redirect', redirectLocation);

        // now we're done constructing url, send the res
        self.send(statusCode, null, {
            Location: redirectLocation
        });

        // tell server to stop processing the handler stack.
        return next(false);
    }
}

/**
 * Flush takes our constructed response object and sends it to the client
 *
 * @private
 * @function flush
 * @param {Response} res - response
 * @param {String|Buffer} body - response body
 * @returns {Response} response
 */
function flush(res, body) {
    assert.ok(
        body === null ||
            body === undefined ||
            typeof body === 'string' ||
            Buffer.isBuffer(body),
        'body must be a string or a Buffer instance'
    );

    res._data = body;

    // Flush headers
    res.writeHead(res.statusCode);

    // Send body if it was provided
    if (res._data) {
        res.write(res._data);
    }

    // Finish request
    res.end();

    // If log level is set to trace, log the entire response object
    if (res.log.trace()) {
        res.log.trace({ res: res }, 'response sent');
    }

    // Return the response object back out to the caller of __send
    return res;
}

/**
 * formatterError is used to handle any case where we were unable to
 * properly format the provided body
 *
 * @private
 * @function formatterError
 * @param {Response} res - response
 * @param {Error} err - error
 * @param {String} [msg] - custom log message
 * @returns {Response} response
 */
function formatterError(res, err, msg) {
    // If the user provided a non-success error code, we don't want to
    // mess with it since their error is probably more important than
    // our inability to format their message.
    if (res.statusCode >= 200 && res.statusCode < 300) {
        res.statusCode = err.statusCode;
    }

    if (typeof msg !== 'string') {
        msg = 'error retrieving formatter';
    }

    res.log.warn(
        {
            req: res.req,
            err: err
        },
        msg
    );

    return flush(res);
}

module.exports = patch;
