/***********************************************************************************************************************************
HTTP Response
***********************************************************************************************************************************/
#include <build.h>

#include "common/debug.h"
#include "common/io/bufferRead.h"
#include "common/io/bufferWrite.h"
#include "common/io/http/client.h"
#include "common/io/http/common.h"
#include "common/io/http/request.h"
#include "common/io/http/response.h"
#include "common/io/io.h"
#include "common/io/read.h"
#include "common/log.h"
#include "common/stat.h"
#include "common/wait.h"

/***********************************************************************************************************************************
HTTP constants
***********************************************************************************************************************************/
#define HTTP_HEADER_CONNECTION                                      "connection"
STRING_STATIC(HTTP_HEADER_CONNECTION_STR,                           HTTP_HEADER_CONNECTION);
#define HTTP_HEADER_TRANSFER_ENCODING                               "transfer-encoding"
STRING_STATIC(HTTP_HEADER_TRANSFER_ENCODING_STR,                    HTTP_HEADER_TRANSFER_ENCODING);

#define HTTP_VALUE_CONNECTION_CLOSE                                 "close"
STRING_STATIC(HTTP_VALUE_CONNECTION_CLOSE_STR,                      HTTP_VALUE_CONNECTION_CLOSE);
#define HTTP_VALUE_TRANSFER_ENCODING_CHUNKED                        "chunked"
STRING_STATIC(HTTP_VALUE_TRANSFER_ENCODING_CHUNKED_STR,             HTTP_VALUE_TRANSFER_ENCODING_CHUNKED);

/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
struct HttpResponse
{
    HttpResponsePub pub;                                            // Publicly accessible variables
    HttpSession *session;                                           // HTTP session
    bool contentChunked;                                            // Is the response content chunked?
    bool contentSizeSet;                                            // Was content size specified?
    uint64_t contentSize;                                           // Content size (ignored for chunked)
    uint64_t contentRemaining;                                      // Content remaining (per chunk if chunked)
    bool closeOnContentEof;                                         // Will server close after content is sent?
    bool contentExists;                                             // Does content exist?
    bool contentEof;                                                // Has all content been read?
    Buffer *content;                                                // Caches content once requested
};

struct HttpResponseMulti
{
    const Buffer *content;                                          // Response content
    Buffer *boundary;                                               // Multipart boundary
    const uint8_t *boundaryLast;                                    // Last boundary location
};

/***********************************************************************************************************************************
When response is done close/reuse the connection
***********************************************************************************************************************************/
static void
httpResponseDone(HttpResponse *const this)
{
    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(HTTP_RESPONSE, this);
    FUNCTION_LOG_END();

    ASSERT(this != NULL);
    ASSERT(this->session != NULL);

    // If close was requested by the server then free the session
    if (this->closeOnContentEof)
    {
        httpSessionFree(this->session);

        // Only update the close stats after a successful response so it is not counted if there was an error/retry
        statInc(HTTP_STAT_CLOSE_STR);
    }
    // Else return it to the client so it can be reused
    else
        httpSessionDone(this->session);

    this->session = NULL;

    FUNCTION_LOG_RETURN_VOID();
}

/***********************************************************************************************************************************
Read content
***********************************************************************************************************************************/
// Helper to determine if it is OK to accept unexpected EOF, e.g. the server closed the socket with properly closing the TLS
// connection. The idea is that since these types of responses can be validated we should be able to detect a short read.
static bool
httpResponseReadIgnoreUnexpectedEof(const HttpResponse *const this)
{
    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(HTTP_RESPONSE, this);
    FUNCTION_LOG_END();

    const String *const contentType = httpHeaderGet(this->pub.header, HTTP_HEADER_CONTENT_TYPE_STR);

    FUNCTION_LOG_RETURN(
        BOOL, strEq(contentType, HTTP_HEADER_CONTENT_TYPE_XML_STR) || strEq(contentType, HTTP_HEADER_CONTENT_TYPE_JSON_STR));
}

static size_t
httpResponseRead(THIS_VOID, Buffer *const buffer, const bool block)
{
    THIS(HttpResponse);

    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(HTTP_RESPONSE, this);
        FUNCTION_LOG_PARAM(BUFFER, buffer);
        FUNCTION_LOG_PARAM(BOOL, block);
    FUNCTION_LOG_END();

    ASSERT(this != NULL);
    ASSERT(buffer != NULL);
    ASSERT(!bufFull(buffer));
    ASSERT(this->contentEof || this->session != NULL);

    // Read if EOF has not been reached
    size_t actualBytes = 0;

    if (!this->contentEof)
    {
        MEM_CONTEXT_TEMP_BEGIN()
        {
            // If close was requested and no content specified then the server may send content up until the eof
            if (this->closeOnContentEof && !this->contentChunked && this->contentSize == 0)
            {
                IoRead *const rawRead = httpSessionIoReadP(
                    this->session, .ignoreUnexpectedEof = httpResponseReadIgnoreUnexpectedEof(this));

                ioRead(rawRead, buffer);
                this->contentEof = ioReadEof(rawRead);
            }
            // Else read using specified encoding or size
            else
            {
                IoRead *const rawRead = httpSessionIoReadP(this->session);

                do
                {
                    // If chunked content and no content remaining
                    if (this->contentChunked && this->contentRemaining == 0)
                    {
                        // Read length of next chunk
                        const String *chunkHeader = strTrim(ioReadLine(rawRead));
                        const int chunkExtPos = strChr(chunkHeader, ';');

                        if (chunkExtPos != -1)
                            chunkHeader = strTrim(strSubN(chunkHeader, 0, (size_t)chunkExtPos));

                        this->contentRemaining = cvtZToUInt64Base(strZ(chunkHeader), 16);

                        // A zero-size chunk terminates the response
                        if (this->contentRemaining == 0)
                        {
                            // Consume chunk trailers and the blank line that terminates them.
                            while (strSize(strTrim(ioReadLine(rawRead))) != 0);

                            this->contentEof = true;
                        }
                    }

                    // Read if there is content remaining
                    if (this->contentRemaining > 0)
                    {
                        // If the buffer is larger than the content that needs to be read then limit the buffer size so the read
                        // won't block or read too far. Casting to size_t is safe on 32-bit because we know the max buffer size is
                        // defined as less than 2^32 so content remaining can't be more than that.
                        if (bufRemains(buffer) > this->contentRemaining)
                            bufLimitSet(buffer, bufSize(buffer) - (bufRemains(buffer) - (size_t)this->contentRemaining));

                        actualBytes = bufRemains(buffer);
                        this->contentRemaining -= ioRead(rawRead, buffer);

                        // Error if EOF but content read is not complete
                        if (ioReadEof(rawRead))
                            THROW(FileReadError, "unexpected EOF reading HTTP content");

                        // Clear limit (this works even if the limit was not set and it is easier than checking)
                        bufLimitClear(buffer);
                    }

                    // If no content remaining
                    if (this->contentRemaining == 0)
                    {
                        // If chunked then consume the blank line that follows every chunk. There might be more chunk data so loop
                        // back around to check.
                        if (this->contentChunked && !this->contentEof)
                        {
                            ioReadLine(rawRead);
                        }
                        // If total content size was provided then this is eof
                        else
                            this->contentEof = true;
                    }
                }
                while (!bufFull(buffer) && !this->contentEof);
            }

            // If all content has been read
            if (this->contentEof)
                httpResponseDone(this);
        }
        MEM_CONTEXT_TEMP_END();
    }

    FUNCTION_LOG_RETURN(SIZE, (size_t)actualBytes);
}

/***********************************************************************************************************************************
Has all content been read?
***********************************************************************************************************************************/
static bool
httpResponseEof(THIS_VOID)
{
    THIS(HttpResponse);

    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(HTTP_RESPONSE, this);
    FUNCTION_LOG_END();

    ASSERT(this != NULL);

    FUNCTION_LOG_RETURN(BOOL, this->contentEof);
}

/***********************************************************************************************************************************
Read response status
***********************************************************************************************************************************/
static void
httpResponseStatusRead(HttpResponse *const this, IoRead *const read)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(HTTP_RESPONSE, this);
        FUNCTION_TEST_PARAM(IO_READ, read);
    FUNCTION_TEST_END();

    MEM_CONTEXT_TEMP_BEGIN()
    {
        // Read status
        String *status = ioReadLine(read);

        // Check status ends with a CR and remove it to make error formatting easier and more accurate
        if (!strEndsWith(status, CR_STR))
            THROW_FMT(FormatError, "HTTP response status '%s' should be CR-terminated", strZ(status));

        status = strSubN(status, 0, strSize(status) - 1);

        // Check status is at least the minimum required length to avoid harder to interpret errors later on
        if (strSize(status) < sizeof(HTTP_VERSION) + 4)
            THROW_FMT(FormatError, "HTTP response '%s' has invalid length", strZ(strTrim(status)));

        // If HTTP/1.0 then the connection will be closed on content eof since connections are not reused by default
        if (strBeginsWith(status, HTTP_VERSION_10_STR))
        {
            this->closeOnContentEof = true;
        }
        // Else check that the version is the default (1.1)
        else if (!strBeginsWith(status, HTTP_VERSION_STR))
            THROW_FMT(FormatError, "HTTP version of response '%s' must be " HTTP_VERSION " or " HTTP_VERSION_10, strZ(status));

        // Read status code
        status = strSub(status, sizeof(HTTP_VERSION));

        int spacePos = strChr(status, ' ');

        if (spacePos != 3)
            THROW_FMT(FormatError, "response status '%s' must have a space after the status code", strZ(status));

        this->pub.code = cvtZSubNToUInt(strZ(status), 0, (size_t)spacePos);

        // Read reason phrase. A missing reason phrase will be represented as an empty string.
        MEM_CONTEXT_OBJ_BEGIN(this)
        {
            this->pub.reason = strSub(status, (size_t)spacePos + 1);
        }
        MEM_CONTEXT_OBJ_END();
    }
    MEM_CONTEXT_TEMP_END();

    FUNCTION_TEST_RETURN_VOID();
}

/***********************************************************************************************************************************
Read response headers
***********************************************************************************************************************************/
static void
httpResponseHeaderRead(HttpResponse *const this, IoRead *const read, const bool allowEof)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(HTTP_RESPONSE, this);
        FUNCTION_TEST_PARAM(IO_READ, read);
        FUNCTION_TEST_PARAM(BOOL, allowEof);
    FUNCTION_TEST_END();

    MEM_CONTEXT_TEMP_BEGIN()
    {
        do
        {
            // Read the next header. In a multipart part the headers may be terminated by the boundary rather than a blank line (the
            // boundary's leading CRLF doubles as the terminator), so allow eof to end the header block in that case.
            String *const header = strTrim(ioReadLineParam(read, allowEof));

            // If the header is empty then we have reached the end of the headers
            if (strSize(header) == 0)
                break;

            // Split the header and store it
            int colonPos = strChr(header, ':');

            if (colonPos < 0)
                THROW_FMT(FormatError, "header '%s' missing colon", strZ(strTrim(header)));

            const String *const headerKey = strLower(strTrim(strSubN(header, 0, (size_t)colonPos)));
            String *const headerValue = strTrim(strSub(header, (size_t)colonPos + 1));

            // Read transfer encoding (only chunked is supported)
            if (strEq(headerKey, HTTP_HEADER_TRANSFER_ENCODING_STR))
            {
                // Error if transfer encoding is not chunked
                if (!strEq(strLower(headerValue), HTTP_VALUE_TRANSFER_ENCODING_CHUNKED_STR))
                {
                    THROW_FMT(
                        FormatError, "only '%s' is supported for '%s' header", HTTP_VALUE_TRANSFER_ENCODING_CHUNKED,
                        HTTP_HEADER_TRANSFER_ENCODING);
                }

                this->contentChunked = true;
            }

            // Read content size
            if (strEq(headerKey, HTTP_HEADER_CONTENT_LENGTH_STR))
            {
                this->contentSizeSet = true;
                this->contentSize = cvtZToUInt64(strZ(headerValue));
                this->contentRemaining = this->contentSize;
            }

            // If the server notified of a closed connection then close the client connection after reading content. This
            // prevents doing a retry on the next request when using the closed connection.
            if (strEq(headerKey, HTTP_HEADER_CONNECTION_STR) && strEq(strLower(headerValue), HTTP_VALUE_CONNECTION_CLOSE_STR))
                this->closeOnContentEof = true;

            // Add after header checks in case the value was modified
            httpHeaderAdd(this->pub.header, headerKey, headerValue);
        }
        while (1);

        // Error if transfer encoding and content length are both set
        if (this->contentChunked && this->contentSizeSet)
        {
            THROW_FMT(
                FormatError, "'%s' and '%s' headers are both set", HTTP_HEADER_TRANSFER_ENCODING,
                HTTP_HEADER_CONTENT_LENGTH);
        }
    }
    MEM_CONTEXT_TEMP_END();

    FUNCTION_TEST_RETURN_VOID();
}

/**********************************************************************************************************************************/
// Constructor helper
static HttpResponse *
httpResponseNewInternal(void)
{
    FUNCTION_TEST_VOID();

    OBJ_NEW_BEGIN(HttpResponse, .childQty = MEM_CONTEXT_QTY_MAX)
    {
        *this = (HttpResponse)
        {
            .pub =
            {
                .header = httpHeaderNew(NULL),
            },
        };
    }
    OBJ_NEW_END();

    FUNCTION_TEST_RETURN(HTTP_RESPONSE, this);
}

FN_EXTERN HttpResponse *
httpResponseNew(HttpSession *const session, const String *const verb, const bool contentCache)
{
    FUNCTION_LOG_BEGIN(logLevelDebug);
        FUNCTION_LOG_PARAM(HTTP_SESSION, session);
        FUNCTION_LOG_PARAM(STRING, verb);
        FUNCTION_LOG_PARAM(BOOL, contentCache);
    FUNCTION_LOG_END();

    ASSERT(session != NULL);
    ASSERT(verb != NULL);

    HttpResponse *const this = httpResponseNewInternal();
    this->session = httpSessionMove(session, objMemContext(this));

    // Read status
    httpResponseStatusRead(this, httpSessionIoReadP(this->session));

    // Read headers
    httpResponseHeaderRead(this, httpSessionIoReadP(this->session), false);

    // Was content returned in the response? HEAD will report content but not actually return any.
    this->contentExists =
        (this->contentChunked || this->contentSize > 0 || this->closeOnContentEof) && !strEq(verb, HTTP_VERB_HEAD_STR);
    this->contentEof = !this->contentExists;

    // Create an io object, even if there is no content. This makes the logic for readers easier -- they can just check eof
    // rather than also checking if the io object exists.
    MEM_CONTEXT_OBJ_BEGIN(this)
    {
        this->pub.contentRead = ioReadNewP(this, .eof = httpResponseEof, .read = httpResponseRead);
        ioReadOpen(httpResponseIoRead(this));
    }
    MEM_CONTEXT_OBJ_END();

    // If there is no content then we are done with the client
    if (!this->contentExists)
    {
        httpResponseDone(this);
    }
    // Else cache content when requested or on error
    else if (contentCache || !httpResponseCodeOk(this))
    {
        MEM_CONTEXT_OBJ_BEGIN(this)
        {
            httpResponseContent(this);
        }
        MEM_CONTEXT_OBJ_END();
    }

    FUNCTION_LOG_RETURN(HTTP_RESPONSE, this);
}

/**********************************************************************************************************************************/
FN_EXTERN const Buffer *
httpResponseContent(HttpResponse *const this)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(HTTP_RESPONSE, this);
    FUNCTION_TEST_END();

    ASSERT(this != NULL);

    if (this->content == NULL)
    {
        this->content = bufNew(0);

        if (this->contentExists)
        {
            do
            {
                bufResize(this->content, bufSize(this->content) + ioBufferSize());
                httpResponseRead(this, this->content, true);
            }
            while (!httpResponseEof(this));

            bufResize(this->content, bufUsed(this->content));
        }
    }

    FUNCTION_TEST_RETURN(BUFFER, this->content);
}

/**********************************************************************************************************************************/
FN_EXTERN bool
httpResponseCodeRetry(const HttpResponse *const this)
{
    return
        httpResponseCode(this) / 100 == HTTP_RESPONSE_CODE_CLASS_RETRY ||
        httpResponseCode(this) == HTTP_RESPONSE_CODE_REQUEST_TIMEOUT ||
        httpResponseCode(this) == HTTP_RESPONSE_CODE_TOO_MANY_REQUESTS;
}

/**********************************************************************************************************************************/
FN_EXTERN HttpResponseMulti *
httpResponseMultiNew(const Buffer *const content, const String *const contentType)
{
    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(BUFFER, content);
        FUNCTION_LOG_PARAM(STRING, contentType);
    FUNCTION_LOG_END();

    ASSERT(content != NULL);

    OBJ_NEW_BEGIN(HttpResponseMulti, .childQty = MEM_CONTEXT_QTY_MAX)
    {
        *this = (HttpResponseMulti)
        {
            .content = content,
        };

        MEM_CONTEXT_TEMP_BEGIN()
        {
            // Extract boundary from content type header
            if (contentType == NULL || !strBeginsWith(contentType, STRDEF(HTTP_HEADER_CONTENT_TYPE_MULTIPART)))
                THROW(FormatError, "expected multipart content type");

            const String *boundary = strSub(contentType, sizeof(HTTP_HEADER_CONTENT_TYPE_MULTIPART) - 1);

            if (strZ(boundary)[0] == '"')
                boundary = strSubN(boundary, 1, strSize(boundary) - 2);

            const String *const boundaryNext = strNewFmt(HTTP_MULTIPART_BOUNDARY_PRE "%s", strZ(boundary));

            MEM_CONTEXT_PRIOR_BEGIN()
            {
                this->boundary = bufNewC(strZ(boundaryNext), strSize(boundaryNext));
            }
            MEM_CONTEXT_PRIOR_END();

            // Find first boundary
            const String *const boundaryFirst = strNewFmt("--%s" HTTP_MULTIPART_BOUNDARY_POST, strZ(boundary));
            ASSERT(strSize(boundaryFirst) == strSize(boundaryNext));
            this->boundaryLast = bufFindP(this->content, BUFSTR(boundaryFirst));

            if (this->boundaryLast == NULL)
                THROW_FMT(FormatError, "multipart boundary '%s' not found", strZ(boundary));
        }
        MEM_CONTEXT_TEMP_END();
    }
    OBJ_NEW_END();

    FUNCTION_LOG_RETURN(HTTP_RESPONSE_MULTI, this);
}

/**********************************************************************************************************************************/
FN_EXTERN HttpResponse *
httpResponseMultiNext(HttpResponseMulti *const this)
{
    FUNCTION_LOG_BEGIN(logLevelTrace);
        FUNCTION_LOG_PARAM(HTTP_RESPONSE_MULTI, this);
    FUNCTION_LOG_END();

    ASSERT(this != NULL);

    HttpResponse *result = NULL;

    // Find next boundary
    this->boundaryLast += bufSize(this->boundary);
    const uint8_t *const boundaryNext = bufFindP(this->content, this->boundary, .begin = this->boundaryLast);

    if (boundaryNext != NULL)
    {
        result = httpResponseNewInternal();

        MEM_CONTEXT_TEMP_BEGIN()
        {
            // Create a buffer for the response
            Buffer *const response = bufNewC(this->boundaryLast, (size_t)(boundaryNext - this->boundaryLast));
            IoRead *const responseIo = ioBufferReadNewOpen(response);

            // Read multipart headers
            httpResponseHeaderRead(result, responseIo, true);

            CHECK(
                FormatError,
                strEq(httpHeaderGet(httpResponseHeader(result), HTTP_HEADER_CONTENT_TYPE_STR), HTTP_HEADER_CONTENT_TYPE_HTTP_STR),
                "expected '" HTTP_HEADER_CONTENT_TYPE ":" HTTP_HEADER_CONTENT_TYPE_HTTP "'");

            // Read status
            httpResponseStatusRead(result, responseIo);

            // Read headers (eof is allowed since the boundary, not a blank line, may terminate the embedded response headers)
            httpResponseHeaderRead(result, responseIo, true);

            // Read content
            CHECK(FormatError, !result->contentChunked, "chunked encoding not supported in multipart");

            MEM_CONTEXT_OBJ_BEGIN(result)
            {
                result->content = bufNew((size_t)result->contentSize);
            }
            MEM_CONTEXT_OBJ_END();

            IoWrite *const write = ioBufferWriteNewOpen(result->content);
            ioCopyP(responseIo, write);
            ioWriteClose(write);

            // Set last boundary
            this->boundaryLast = boundaryNext + 2;
        }
        MEM_CONTEXT_TEMP_END();
    }

    FUNCTION_LOG_RETURN(HTTP_RESPONSE, result);
}

/**********************************************************************************************************************************/
FN_EXTERN void
httpResponseToLog(const HttpResponse *const this, StringStatic *const debugLog)
{
    strStcFmt(
        debugLog,
        "{code: %u, reason: %s, contentChunked: %s, contentSize: %" PRIu64 ", contentRemaining: %" PRIu64 ", closeOnContentEof: %s"
        ", contentExists: %s, contentEof: %s, contentCached: %s}",
        httpResponseCode(this), httpResponseReason(this) == NULL ? NULL_Z : strZ(httpResponseReason(this)),
        cvtBoolToConstZ(this->contentChunked), this->contentSize, this->contentRemaining, cvtBoolToConstZ(this->closeOnContentEof),
        cvtBoolToConstZ(this->contentExists), cvtBoolToConstZ(this->contentEof), cvtBoolToConstZ(this->content != NULL));

    strStcCat(debugLog, ", header: "),
    httpHeaderToLog(httpResponseHeader(this), debugLog);
    strStcCatChr(debugLog, '}');
}
