/*
 *  IXHttp.cpp
 *  Author: Benjamin Sergeant
 *  Copyright (c) 2019 Machine Zone, Inc. All rights reserved.
 */

#include "IXHttp.h"

#include "IXCancellationRequest.h"
#include "IXGzipCodec.h"
#include "IXSocket.h"
#include <sstream>
#include <vector>

namespace ix
{
    std::string Http::trim(const std::string& str)
    {
        std::string out;
        for (auto c : str)
        {
            if (c != ' ' && c != '\n' && c != '\r')
            {
                out += c;
            }
        }

        return out;
    }

    std::pair<std::string, int> Http::parseStatusLine(const std::string& line)
    {
        // Request-Line   = Method SP Request-URI SP HTTP-Version CRLF
        std::string token;
        std::stringstream tokenStream(line);
        std::vector<std::string> tokens;

        // Split by ' '
        while (std::getline(tokenStream, token, ' '))
        {
            tokens.push_back(token);
        }

        std::string httpVersion;
        if (tokens.size() >= 1)
        {
            httpVersion = trim(tokens[0]);
        }

        int statusCode = -1;
        if (tokens.size() >= 2)
        {
            std::stringstream ss;
            ss << trim(tokens[1]);
            ss >> statusCode;
        }

        return std::make_pair(httpVersion, statusCode);
    }

    std::tuple<std::string, std::string, std::string> Http::parseRequestLine(
        const std::string& line)
    {
        // Request-Line   = Method SP Request-URI SP HTTP-Version CRLF
        std::string token;
        std::stringstream tokenStream(line);
        std::vector<std::string> tokens;

        // Split by ' '
        while (std::getline(tokenStream, token, ' '))
        {
            tokens.push_back(token);
        }

        std::string method;
        if (tokens.size() >= 1)
        {
            method = trim(tokens[0]);
        }

        std::string requestUri;
        if (tokens.size() >= 2)
        {
            requestUri = trim(tokens[1]);
        }

        std::string httpVersion;
        if (tokens.size() >= 3)
        {
            httpVersion = trim(tokens[2]);
        }

        return std::make_tuple(method, requestUri, httpVersion);
    }

    std::tuple<bool, std::string, HttpRequestPtr> Http::parseRequest(
        std::unique_ptr<Socket>& socket, int timeoutSecs)
    {
        HttpRequestPtr httpRequest;

        std::atomic<bool> requestInitCancellation(false);

        auto isCancellationRequested =
            makeCancellationRequestWithTimeout(timeoutSecs, requestInitCancellation);

        // Read first line
        auto lineResult = socket->readLine(isCancellationRequested);
        auto lineValid = lineResult.first;
        auto line = lineResult.second;

        if (!lineValid)
        {
            return std::make_tuple(false, "Error reading HTTP request line", httpRequest);
        }

        // Parse request line (GET /foo HTTP/1.1\r\n)
        auto requestLine = Http::parseRequestLine(line);
        auto method = std::get<0>(requestLine);
        auto uri = std::get<1>(requestLine);
        auto httpVersion = std::get<2>(requestLine);

        // Retrieve and validate HTTP headers
        auto result = parseHttpHeaders(socket, isCancellationRequested);
        auto headersValid = result.first;
        auto headers = result.second;

        if (!headersValid)
        {
            return std::make_tuple(false, "Error parsing HTTP headers", httpRequest);
        }

        std::string body;
        if (headers.find("Content-Length") != headers.end())
        {
            int contentLength = 0;
            {
                const char* p = headers["Content-Length"].c_str();
                char* p_end {};
                errno = 0;
                long val = std::strtol(p, &p_end, 10);
                if (p_end == p         // invalid argument
                    || errno == ERANGE // out of range
                )
                {
                    return std::make_tuple(
                        false, "Error parsing HTTP Header 'Content-Length'", httpRequest);
                }
                if (val > std::numeric_limits<int>::max())
                {
                    return std::make_tuple(
                        false, "Error: 'Content-Length' value was above max", httpRequest);
                }
                if (val < std::numeric_limits<int>::min())
                {
                    return std::make_tuple(
                        false, "Error: 'Content-Length' value was below min", httpRequest);
                }
                contentLength = static_cast<int>(val);
            }
            if (contentLength < 0)
            {
                return std::make_tuple(
                    false, "Error: 'Content-Length' should be a positive integer", httpRequest);
            }

            auto res = socket->readBytes(contentLength, nullptr, nullptr, isCancellationRequested);
            if (!res.first)
            {
                return std::make_tuple(
                    false, std::string("Error reading request: ") + res.second, httpRequest);
            }
            body = res.second;
        }

        // If the content was compressed with gzip, decode it
        if (headers["Content-Encoding"] == "gzip")
        {
#ifdef IXWEBSOCKET_USE_ZLIB
            std::string decompressedPayload;
            if (!gzipDecompress(body, decompressedPayload))
            {
                return std::make_tuple(
                    false, std::string("Error during gzip decompression of the body"), httpRequest);
            }
            body = decompressedPayload;
#else
            std::string errorMsg("ixwebsocket was not compiled with gzip support on");
            return std::make_tuple(false, errorMsg, httpRequest);
#endif
        }

        httpRequest = std::make_shared<HttpRequest>(uri, method, httpVersion, body, headers);
        return std::make_tuple(true, "", httpRequest);
    }

    bool Http::sendResponse(HttpResponsePtr response, std::unique_ptr<Socket>& socket)
    {
        // Write the response to the socket
        std::stringstream ss;
        ss << "HTTP/1.1 ";
        ss << response->statusCode;
        ss << " ";
        ss << response->description;
        ss << "\r\n";

        if (!socket->writeBytes(ss.str(), nullptr))
        {
            return false;
        }

        // Write headers
        ss.str("");
        ss << "Content-Length: " << response->body.size() << "\r\n";
        for (auto&& it : response->headers)
        {
            ss << it.first << ": " << it.second << "\r\n";
        }
        ss << "\r\n";

        if (!socket->writeBytes(ss.str(), nullptr))
        {
            return false;
        }

        return response->body.empty() ? true : socket->writeBytes(response->body, nullptr);
    }
} // namespace ix
