/*
 * AWS IoT Jobs V1.0.1
 * Copyright (C) 2020 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/**
 * @file aws_iot_jobs_serialize.c
 * @brief Implements functions that generate and parse Jobs JSON documents.
 */

/* The config header is always included first. */
#include "iot_config.h"

/* Standard includes. */
#include <stdio.h>
#include <string.h>

/* Jobs internal include. */
#include "private/aws_iot_jobs_internal.h"

/* JSON utilities include. */
#include "aws_iot_doc_parser.h"

/**
 * @brief Minimum length of a Jobs request.
 *
 * At the very least, the request will contain: {"clientToken":""}
 */
#define MINIMUM_REQUEST_LENGTH                    ( AWS_IOT_CLIENT_TOKEN_KEY_LENGTH + 7 )

/**
 * @brief The length of client tokens generated by this library.
 */
#define CLIENT_TOKEN_AUTOGENERATE_LENGTH          ( 8 )

/**
 * @brief JSON key representing Jobs status.
 */
#define STATUS_KEY                                "status"

/**
 * @brief Length of #STATUS_KEY.
 */
#define STATUS_KEY_LENGTH                         ( sizeof( STATUS_KEY ) - 1 )

/**
 * @brief JSON key representing Jobs status details.
 */
#define STATUS_DETAILS_KEY                        "statusDetails"

/**
 * @brief Length of #STATUS_DETAILS_KEY.
 */
#define STATUS_DETAILS_KEY_LENGTH                 ( sizeof( STATUS_DETAILS_KEY ) - 1 )

/**
 * @brief JSON key representing Jobs expected version.
 */
#define EXPECTED_VERSION_KEY                      "expectedVersion"

/**
 * @brief Length of #EXPECTED_VERSION_KEY.
 */
#define EXPECTED_VERSION_KEY_LENGTH               ( sizeof( EXPECTED_VERSION_KEY ) - 1 )

/**
 * @brief Maximum length of the expected version when represented as a string.
 *
 * The expected version is a 32-bit unsigned integer. This can be represented in
 * 10 digits plus a NULL-terminator.
 */
#define EXPECTED_VERSION_STRING_LENGTH            ( 11 )

/**
 * @brief JSON key representing Jobs step timeout.
 */
#define STEP_TIMEOUT_KEY                          "stepTimeoutInMinutes"

/**
 * @brief Length of #STEP_TIMEOUT_KEY.
 */
#define STEP_TIMEOUT_KEY_LENGTH                   ( sizeof( STEP_TIMEOUT_KEY ) - 1 )

/**
 * @brief Maximum length of the step timeout when represented as a string.
 *
 * The step timeout is in the range of [-1,10080]. This can be represented as
 * 5 digits plus a NULL-terminator.
 */
#define STEP_TIMEOUT_STRING_LENGTH                ( 6 )

/**
 * @brief JSON key representing the "include Job document" flag.
 */
#define INCLUDE_JOB_DOCUMENT_KEY                  "includeJobDocument"

/**
 * @brief JSON key representing the "include Job Execution state" flag.
 */
#define INCLUDE_JOB_EXECUTION_STATE_KEY           "includeJobExecutionState"

/**
 * @brief Length of #INCLUDE_JOB_EXECUTION_STATE_KEY.
 */
#define INCLUDE_JOB_EXECUTION_STATE_KEY_LENGTH    ( sizeof( INCLUDE_JOB_EXECUTION_STATE_KEY ) - 1 )

/**
 * @brief Length of #INCLUDE_JOB_DOCUMENT_KEY.
 */
#define INCLUDE_JOB_DOCUMENT_KEY_LENGTH           ( sizeof( INCLUDE_JOB_DOCUMENT_KEY ) - 1 )

/**
 * @brief JSON key representing the Jobs execution number.
 */
#define EXECUTION_NUMBER_KEY                      "executionNumber"

/**
 * @brief Length of #EXECUTION_NUMBER_KEY.
 */
#define EXECUTION_NUMBER_KEY_LENGTH               ( sizeof( EXECUTION_NUMBER_KEY ) - 1 )

/**
 * @brief Maximum length of the execution number when represented as a string.
 *
 * The execution number is a 32-bit integer. This can be represented in 10 digits,
 * plus 1 for a possible negative sign, plus a NULL-terminator.
 */
#define EXECUTION_NUMBER_STRING_LENGTH            ( 12 )

/**
 * @brief JSON key representing Jobs error code in error responses.
 */
#define CODE_KEY                                  "code"

/**
 * @brief Length of #CODE_KEY.
 */
#define CODE_KEY_LENGTH                           ( sizeof( CODE_KEY ) - 1 )

/**
 * @brief Append a string to a buffer.
 *
 * Also updates `copyOffset` with `stringLength`.
 *
 * @param[in] pBuffer Start of a buffer.
 * @param[in] copyOffset Offset in `pBuffer` where `pString` will be placed.
 * @param[in] pString The string to append.
 * @param[in] stringLength Length of `pString`.
 */
#define APPEND_STRING( pBuffer, copyOffset, pString, stringLength ) \
    ( void ) memcpy( pBuffer + copyOffset, pString, stringLength ); \
    copyOffset += ( size_t ) stringLength;

/*-----------------------------------------------------------*/

/**
 * @brief Place a JSON boolean flag in the given buffer.
 *
 * @param[in] pBuffer The buffer where the flag is placed.
 * @param[in] copyOffset Offset in `pBuffer` where the flag is placed.
 * @param[in] pFlagName Either #INCLUDE_JOB_DOCUMENT_KEY or #INCLUDE_JOB_EXECUTION_STATE_KEY.
 * @param[in] flagNameLength Either #INCLUDE_JOB_EXECUTION_STATE_KEY_LENGTH or
 * #INCLUDE_JOB_EXECUTION_STATE_KEY_LENGTH
 * @param[in] value Either `true` or `false`.
 *
 * @warning This function does not check the length of `pBuffer`! Any provided
 * buffer must be large enough to accommodate the flag and value.
 *
 * @return A value of `copyOffset` after the flag.
 */
static size_t _appendFlag( char * pBuffer,
                           size_t copyOffset,
                           const char * pFlagName,
                           size_t flagNameLength,
                           bool value );

/**
 * @brief Place Job status details in the given buffer.
 *
 * @param[in] pBuffer The buffer where the status details are placed.
 * @param[in] copyOffset Offset in `pBuffer` where the status details are placed.
 * @param[in] pStatusDetails The status details to place in the buffer.
 * @param[in] statusDetailsLength Length of `pStatusDetails`.
 *
 * @warning This function does not check the length of `pBuffer`! Any provided
 * buffer must be large enough to accommodate the status details.
 *
 * @return A value of `copyOffset` after the status details.
 */
static size_t _appendStatusDetails( char * pBuffer,
                                    size_t copyOffset,
                                    const char * pStatusDetails,
                                    size_t statusDetailsLength );

/**
 * @brief Place Job execution number in the given buffer.
 *
 * @param[in] pBuffer The buffer where the execution number is placed.
 * @param[in] copyOffset Offset in `pBuffer` where the execution number is placed.
 * @param[in] pExecutionNumber The execution number to place in the buffer.
 * @param[in] executionNumberLength Length of `pExecutionNumber`.
 *
 * @warning This function does not check the length of `pBuffer`! Any provided
 * buffer must be large enough to accommodate the execution number.
 *
 * @return A value of `copyOffset` after the execution number.
 */
static size_t _appendExecutionNumber( char * pBuffer,
                                      size_t copyOffset,
                                      const char * pExecutionNumber,
                                      size_t executionNumberLength );

/**
 * @brief Place Job step timeout in the given buffer.
 *
 * @param[in] pBuffer The buffer where the step timeout is placed.
 * @param[in] copyOffset Offset in `pBuffer` where the step timeout is placed.
 * @param[in] pStepTimeout The step timeout to place in the buffer.
 * @param[in] stepTimeoutLength Length of `pStepTimeout`.
 *
 * @warning This function does not check the length of `pBuffer`! Any provided
 * buffer must be large enough to accommodate the step timeout.
 *
 * @return A value of `copyOffset` after the step timeout.
 */
static size_t _appendStepTimeout( char * pBuffer,
                                  size_t copyOffset,
                                  const char * pStepTimeout,
                                  size_t stepTimeoutLength );

/**
 * @brief Place a client token in the given buffer.
 *
 * @param[in] pBuffer The buffer where the client token is placed.
 * @param[in] copyOffset Offset in `pBuffer` where client token is placed.
 * @param[in] pRequestInfo Contains information on a client token to place.
 * @param[out] pOperation Location and length of client token are written here.
 *
 * @warning This function does not check the length of `pBuffer`! Any provided
 * buffer must be large enough to accommodate #CLIENT_TOKEN_AUTOGENERATE_LENGTH
 * characters.
 *
 * @return A value of `copyOffset` after the client token.
 */
static size_t _appendClientToken( char * pBuffer,
                                  size_t copyOffset,
                                  const AwsIotJobsRequestInfo_t * pRequestInfo,
                                  _jobsOperation_t * pOperation );

/**
 * @brief Generates a request JSON for a GET PENDING operation.
 *
 * @param[in] pRequestInfo Common Jobs request parameters.
 * @param[in] pOperation Operation associated with the Jobs request.
 *
 * @return #AWS_IOT_JOBS_SUCCESS or #AWS_IOT_JOBS_NO_MEMORY
 */
static AwsIotJobsError_t _generateGetPendingRequest( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                                     _jobsOperation_t * pOperation );

/**
 * @brief Generates a request JSON for a START NEXT operation.
 *
 * @param[in] pRequestInfo Common Jobs request parameters.
 * @param[in] pUpdateInfo Jobs update parameters.
 * @param[in] pOperation Operation associated with the Jobs request.
 *
 * @return #AWS_IOT_JOBS_SUCCESS or #AWS_IOT_JOBS_NO_MEMORY
 */
static AwsIotJobsError_t _generateStartNextRequest( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                                    const AwsIotJobsUpdateInfo_t * pUpdateInfo,
                                                    _jobsOperation_t * pOperation );

/**
 * @brief Generates a request JSON for a DESCRIBE operation.
 *
 * @param[in] pRequestInfo Common jobs request parameters.
 * @param[in] executionNumber Job execution number to include in request.
 * @param[in] includeJobDocument Whether the response should include the Job document.
 * @param[in] pOperation Operation associated with the Jobs request.
 *
 * @return #AWS_IOT_JOBS_SUCCESS or #AWS_IOT_JOBS_NO_MEMORY.
 */
static AwsIotJobsError_t _generateDescribeRequest( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                                   int32_t executionNumber,
                                                   bool includeJobDocument,
                                                   _jobsOperation_t * pOperation );

/**
 * @brief Generates a request JSON for an UPDATE operation.
 *
 * @param[in] pRequestInfo Common Jobs request parameters.
 * @param[in] pUpdateInfo Jobs update parameters.
 * @param[in] pOperation Operation associated with the Jobs request.
 */
static AwsIotJobsError_t _generateUpdateRequest( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                                 const AwsIotJobsUpdateInfo_t * pUpdateInfo,
                                                 _jobsOperation_t * pOperation );

/**
 * @brief Parse an error from a Jobs error document.
 *
 * @param[in] pErrorDocument Jobs error document.
 * @param[in] errorDocumentLength Length of `pErrorDocument`.
 *
 * @return A Jobs error code between #AWS_IOT_JOBS_INVALID_TOPIC and
 * #AWS_IOT_JOBS_TERMINAL_STATE.
 */
static AwsIotJobsError_t _parseErrorDocument( const char * pErrorDocument,
                                              size_t errorDocumentLength );

/*-----------------------------------------------------------*/

static size_t _appendFlag( char * pBuffer,
                           size_t copyOffset,
                           const char * pFlagName,
                           size_t flagNameLength,
                           bool value )
{
    if( value == true )
    {
        APPEND_STRING( pBuffer,
                       copyOffset,
                       pFlagName,
                       flagNameLength );
        APPEND_STRING( pBuffer, copyOffset, "\":true,\"", 8 );
    }
    else
    {
        APPEND_STRING( pBuffer,
                       copyOffset,
                       pFlagName,
                       flagNameLength );
        APPEND_STRING( pBuffer, copyOffset, "\":false,\"", 9 );
    }

    return copyOffset;
}

/*-----------------------------------------------------------*/

static size_t _appendStatusDetails( char * pBuffer,
                                    size_t copyOffset,
                                    const char * pStatusDetails,
                                    size_t statusDetailsLength )
{
    APPEND_STRING( pBuffer, copyOffset, STATUS_DETAILS_KEY, STATUS_DETAILS_KEY_LENGTH );
    APPEND_STRING( pBuffer, copyOffset, "\":", 2 );
    APPEND_STRING( pBuffer,
                   copyOffset,
                   pStatusDetails,
                   statusDetailsLength );
    APPEND_STRING( pBuffer, copyOffset, ",\"", 2 );

    return copyOffset;
}

/*-----------------------------------------------------------*/

static size_t _appendExecutionNumber( char * pBuffer,
                                      size_t copyOffset,
                                      const char * pExecutionNumber,
                                      size_t executionNumberLength )
{
    APPEND_STRING( pBuffer,
                   copyOffset,
                   EXECUTION_NUMBER_KEY,
                   EXECUTION_NUMBER_KEY_LENGTH );
    APPEND_STRING( pBuffer,
                   copyOffset,
                   "\":",
                   2 );
    APPEND_STRING( pBuffer,
                   copyOffset,
                   pExecutionNumber,
                   executionNumberLength );
    APPEND_STRING( pBuffer, copyOffset, ",\"", 2 );

    return copyOffset;
}

/*-----------------------------------------------------------*/

static size_t _appendStepTimeout( char * pBuffer,
                                  size_t copyOffset,
                                  const char * pStepTimeout,
                                  size_t stepTimeoutLength )
{
    APPEND_STRING( pBuffer,
                   copyOffset,
                   STEP_TIMEOUT_KEY,
                   STEP_TIMEOUT_KEY_LENGTH );
    APPEND_STRING( pBuffer, copyOffset, "\":", 2 );
    APPEND_STRING( pBuffer, copyOffset, pStepTimeout, stepTimeoutLength );
    APPEND_STRING( pBuffer, copyOffset, ",\"", 2 );

    return copyOffset;
}

/*-----------------------------------------------------------*/

static size_t _appendClientToken( char * pBuffer,
                                  size_t copyOffset,
                                  const AwsIotJobsRequestInfo_t * pRequestInfo,
                                  _jobsOperation_t * pOperation )
{
    int clientTokenLength = 0;
    uint32_t clientToken = 0;

    /* Place the client token key in the buffer. */
    APPEND_STRING( pBuffer,
                   copyOffset,
                   AWS_IOT_CLIENT_TOKEN_KEY,
                   AWS_IOT_CLIENT_TOKEN_KEY_LENGTH );
    APPEND_STRING( pBuffer, copyOffset, "\":\"", 3 );

    /* Set the pointer to the client token. */
    pOperation->pClientToken = pBuffer + copyOffset - 1;

    if( pRequestInfo->pClientToken == AWS_IOT_JOBS_CLIENT_TOKEN_AUTOGENERATE )
    {
        /* Take the address of the given buffer, truncated to 8 characters. This
         * provides a client token that is very likely to be unique while in use. */
        clientToken = ( uint32_t ) ( ( uint64_t ) pBuffer % 100000000ULL );

        clientTokenLength = snprintf( pBuffer + copyOffset,
                                      CLIENT_TOKEN_AUTOGENERATE_LENGTH + 1,
                                      "%08u", clientToken );
        AwsIotJobs_Assert( clientTokenLength == CLIENT_TOKEN_AUTOGENERATE_LENGTH );

        copyOffset += ( size_t ) clientTokenLength;
        pOperation->clientTokenLength = CLIENT_TOKEN_AUTOGENERATE_LENGTH + 2;
    }
    else
    {
        APPEND_STRING( pBuffer,
                       copyOffset,
                       pRequestInfo->pClientToken,
                       pRequestInfo->clientTokenLength );

        pOperation->clientTokenLength = pRequestInfo->clientTokenLength + 2;
    }

    return copyOffset;
}

/*-----------------------------------------------------------*/

static AwsIotJobsError_t _generateGetPendingRequest( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                                     _jobsOperation_t * pOperation )
{
    AwsIotJobsError_t status = AWS_IOT_JOBS_SUCCESS;
    char * pJobsRequest = NULL;
    size_t copyOffset = 0;
    size_t requestLength = MINIMUM_REQUEST_LENGTH;

    /* Add the length of the client token. */
    if( pRequestInfo->pClientToken != AWS_IOT_JOBS_CLIENT_TOKEN_AUTOGENERATE )
    {
        AwsIotJobs_Assert( pRequestInfo->clientTokenLength > 0 );

        requestLength += pRequestInfo->clientTokenLength;
    }
    else
    {
        requestLength += CLIENT_TOKEN_AUTOGENERATE_LENGTH;
    }

    /* Allocate memory for the request JSON. */
    pJobsRequest = AwsIotJobs_MallocString( requestLength );

    if( pJobsRequest == NULL )
    {
        IotLogError( "No memory for Jobs GET PENDING request." );
        status = AWS_IOT_JOBS_NO_MEMORY;
    }
    else
    {
        /* Clear the request JSON. */
        ( void ) memset( pJobsRequest, 0x00, requestLength );

        /* Construct the request JSON, which consists of just a clientToken key. */
        APPEND_STRING( pJobsRequest, copyOffset, "{\"", 2 );
        copyOffset = _appendClientToken( pJobsRequest, copyOffset, pRequestInfo, pOperation );
        APPEND_STRING( pJobsRequest, copyOffset, "\"}", 2 );

        /* Set the output parameters. */
        pOperation->pJobsRequest = pJobsRequest;
        pOperation->jobsRequestLength = requestLength;

        /* Ensure offsets are valid. */
        AwsIotJobs_Assert( copyOffset == requestLength );
        AwsIotJobs_Assert( pOperation->pClientToken > pOperation->pJobsRequest );
        AwsIotJobs_Assert( pOperation->pClientToken <
                           pOperation->pJobsRequest + pOperation->jobsRequestLength );

        IotLogDebug( "Jobs GET PENDING request: %.*s",
                     pOperation->jobsRequestLength,
                     pOperation->pJobsRequest );
    }

    return status;
}

/*-----------------------------------------------------------*/

static AwsIotJobsError_t _generateStartNextRequest( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                                    const AwsIotJobsUpdateInfo_t * pUpdateInfo,
                                                    _jobsOperation_t * pOperation )
{
    AwsIotJobsError_t status = AWS_IOT_JOBS_SUCCESS;
    char * pJobsRequest = NULL;
    size_t copyOffset = 0;
    size_t requestLength = MINIMUM_REQUEST_LENGTH;
    char pStepTimeout[ STEP_TIMEOUT_STRING_LENGTH ] = { 0 };
    int stepTimeoutLength = 0;

    /* Add the length of status details if provided. */
    if( pUpdateInfo->pStatusDetails != AWS_IOT_JOBS_NO_STATUS_DETAILS )
    {
        /* Add 4 for the 2 quotes, colon, and comma. */
        requestLength += STATUS_DETAILS_KEY_LENGTH + 4;
        requestLength += pUpdateInfo->statusDetailsLength;
    }

    if( pUpdateInfo->stepTimeoutInMinutes != AWS_IOT_JOBS_NO_TIMEOUT )
    {
        /* Calculate the length of the step timeout. Add 4 for the 2 quotes, colon, and comma. */
        requestLength += STEP_TIMEOUT_KEY_LENGTH + 4;

        if( pUpdateInfo->stepTimeoutInMinutes == AWS_IOT_JOBS_CANCEL_TIMEOUT )
        {
            /* Step timeout will be set to -1. */
            pStepTimeout[ 0 ] = '-';
            pStepTimeout[ 1 ] = '1';
            stepTimeoutLength = 2;
        }
        else
        {
            /* Convert the step timeout to a string. */
            stepTimeoutLength = snprintf( pStepTimeout,
                                          STEP_TIMEOUT_STRING_LENGTH,
                                          "%d",
                                          pUpdateInfo->stepTimeoutInMinutes );
            AwsIotJobs_Assert( stepTimeoutLength > 0 );
            AwsIotJobs_Assert( stepTimeoutLength < STEP_TIMEOUT_STRING_LENGTH );
        }

        requestLength += ( size_t ) stepTimeoutLength;
    }

    /* Add the length of the client token. */
    if( pRequestInfo->pClientToken != AWS_IOT_JOBS_CLIENT_TOKEN_AUTOGENERATE )
    {
        AwsIotJobs_Assert( pRequestInfo->clientTokenLength > 0 );

        requestLength += pRequestInfo->clientTokenLength;
    }
    else
    {
        requestLength += CLIENT_TOKEN_AUTOGENERATE_LENGTH;
    }

    /* Allocate memory for the request JSON. */
    pJobsRequest = AwsIotJobs_MallocString( requestLength );

    if( pJobsRequest == NULL )
    {
        IotLogError( "No memory for Jobs START NEXT request." );
        status = AWS_IOT_JOBS_NO_MEMORY;
    }
    else
    {
        /* Clear the request JSON. */
        ( void ) memset( pJobsRequest, 0x00, requestLength );

        /* Construct the request JSON. */
        APPEND_STRING( pJobsRequest, copyOffset, "{\"", 2 );

        /* Add status details if present. */
        if( pUpdateInfo->pStatusDetails != AWS_IOT_JOBS_NO_STATUS_DETAILS )
        {
            copyOffset = _appendStatusDetails( pJobsRequest,
                                               copyOffset,
                                               pUpdateInfo->pStatusDetails,
                                               pUpdateInfo->statusDetailsLength );
        }

        /* Add step timeout if present. */
        if( pUpdateInfo->stepTimeoutInMinutes != AWS_IOT_JOBS_NO_TIMEOUT )
        {
            copyOffset = _appendStepTimeout( pJobsRequest,
                                             copyOffset,
                                             pStepTimeout,
                                             stepTimeoutLength );
        }

        /* Add client token. */
        copyOffset = _appendClientToken( pJobsRequest, copyOffset, pRequestInfo, pOperation );

        APPEND_STRING( pJobsRequest, copyOffset, "\"}", 2 );

        /* Set the output parameters. */
        pOperation->pJobsRequest = pJobsRequest;
        pOperation->jobsRequestLength = requestLength;

        /* Ensure offsets are valid. */
        AwsIotJobs_Assert( copyOffset == requestLength );
        AwsIotJobs_Assert( pOperation->pClientToken > pOperation->pJobsRequest );
        AwsIotJobs_Assert( pOperation->pClientToken <
                           pOperation->pJobsRequest + pOperation->jobsRequestLength );

        IotLogDebug( "Jobs START NEXT request: %.*s",
                     pOperation->jobsRequestLength,
                     pOperation->pJobsRequest );
    }

    return status;
}

/*-----------------------------------------------------------*/

static AwsIotJobsError_t _generateDescribeRequest( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                                   int32_t executionNumber,
                                                   bool includeJobDocument,
                                                   _jobsOperation_t * pOperation )
{
    AwsIotJobsError_t status = AWS_IOT_JOBS_SUCCESS;
    char * pJobsRequest = NULL;
    size_t copyOffset = 0;
    size_t requestLength = MINIMUM_REQUEST_LENGTH;
    char pExecutionNumber[ EXECUTION_NUMBER_STRING_LENGTH ] = { 0 };
    int executionNumberLength = 0;

    /* Add the "include job document" flag if false. The default value is true,
     * so the flag is not needed if true. */
    if( includeJobDocument == false )
    {
        /* Add the length of "includeJobDocument" plus 4 for 2 quotes, a colon,
         * and a comma. */
        requestLength += INCLUDE_JOB_DOCUMENT_KEY_LENGTH + 4;

        /* Add the length of "false". */
        requestLength += 5;
    }

    /* Add the length of the execution number if present. */
    if( executionNumber != AWS_IOT_JOBS_NO_EXECUTION_NUMBER )
    {
        /* Convert the execution number to a string. */
        executionNumberLength = snprintf( pExecutionNumber,
                                          EXECUTION_NUMBER_STRING_LENGTH,
                                          "%d",
                                          executionNumber );
        AwsIotJobs_Assert( executionNumberLength > 0 );
        AwsIotJobs_Assert( executionNumberLength < EXECUTION_NUMBER_STRING_LENGTH );

        requestLength += EXECUTION_NUMBER_KEY_LENGTH + 4;
        requestLength += ( size_t ) executionNumberLength;
    }

    /* Add the length of the client token. */
    if( pRequestInfo->pClientToken != AWS_IOT_JOBS_CLIENT_TOKEN_AUTOGENERATE )
    {
        AwsIotJobs_Assert( pRequestInfo->clientTokenLength > 0 );

        requestLength += pRequestInfo->clientTokenLength;
    }
    else
    {
        requestLength += CLIENT_TOKEN_AUTOGENERATE_LENGTH;
    }

    /* Allocate memory for the request JSON. */
    pJobsRequest = AwsIotJobs_MallocString( requestLength );

    if( pJobsRequest == NULL )
    {
        IotLogError( "No memory for Jobs DESCRIBE request." );
        status = AWS_IOT_JOBS_NO_MEMORY;
    }
    else
    {
        /* Clear the request JSON. */
        ( void ) memset( pJobsRequest, 0x00, requestLength );

        /* Construct the request JSON. */
        APPEND_STRING( pJobsRequest, copyOffset, "{\"", 2 );

        /* Add the "include job document" flag if false. */
        if( includeJobDocument == false )
        {
            copyOffset = _appendFlag( pJobsRequest,
                                      copyOffset,
                                      INCLUDE_JOB_DOCUMENT_KEY,
                                      INCLUDE_JOB_DOCUMENT_KEY_LENGTH,
                                      false );
        }

        /* Add the length of the execution number if present. */
        if( executionNumber != AWS_IOT_JOBS_NO_EXECUTION_NUMBER )
        {
            copyOffset = _appendExecutionNumber( pJobsRequest,
                                                 copyOffset,
                                                 pExecutionNumber,
                                                 ( size_t ) executionNumberLength );
        }

        /* Add client token. */
        copyOffset = _appendClientToken( pJobsRequest, copyOffset, pRequestInfo, pOperation );

        APPEND_STRING( pJobsRequest, copyOffset, "\"}", 2 );

        /* Set the output parameters. */
        pOperation->pJobsRequest = pJobsRequest;
        pOperation->jobsRequestLength = requestLength;

        /* Ensure offsets are valid. */
        AwsIotJobs_Assert( copyOffset == requestLength );
        AwsIotJobs_Assert( pOperation->pClientToken > pOperation->pJobsRequest );
        AwsIotJobs_Assert( pOperation->pClientToken <
                           pOperation->pJobsRequest + pOperation->jobsRequestLength );

        IotLogDebug( "Jobs DESCRIBE request: %.*s",
                     pOperation->jobsRequestLength,
                     pOperation->pJobsRequest );
    }

    return status;
}

/*-----------------------------------------------------------*/

static AwsIotJobsError_t _generateUpdateRequest( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                                 const AwsIotJobsUpdateInfo_t * pUpdateInfo,
                                                 _jobsOperation_t * pOperation )
{
    AwsIotJobsError_t status = AWS_IOT_JOBS_SUCCESS;
    char * pJobsRequest = NULL;
    size_t copyOffset = 0;
    size_t requestLength = MINIMUM_REQUEST_LENGTH;
    const char * pStatus = NULL;
    size_t statusLength = 0;
    char pExpectedVersion[ EXPECTED_VERSION_STRING_LENGTH ] = { 0 };
    char pExecutionNumber[ EXECUTION_NUMBER_STRING_LENGTH ] = { 0 };
    char pStepTimeout[ STEP_TIMEOUT_STRING_LENGTH ] = { 0 };
    int expectedVersionLength = 0, executionNumberLength = 0, stepTimeoutLength = 0;

    /* Determine the status string and length to report to the Jobs service.
     * Add 6 for the 4 quotes, colon, and comma. */
    requestLength += STATUS_KEY_LENGTH + 6;

    switch( pUpdateInfo->newStatus )
    {
        case AWS_IOT_JOB_STATE_IN_PROGRESS:
            pStatus = "IN_PROGRESS";
            break;

        case AWS_IOT_JOB_STATE_FAILED:
            pStatus = "FAILED";
            break;

        case AWS_IOT_JOB_STATE_SUCCEEDED:
            pStatus = "SUCCEEDED";
            break;

        default:
            /* The only remaining valid state is REJECTED. */
            AwsIotJobs_Assert( pUpdateInfo->newStatus == AWS_IOT_JOB_STATE_REJECTED );
            pStatus = "REJECTED";
            break;
    }

    statusLength = strlen( pStatus );
    requestLength += statusLength;

    /* Add the length of status details if provided. */
    if( pUpdateInfo->pStatusDetails != AWS_IOT_JOBS_NO_STATUS_DETAILS )
    {
        /* Add 4 for the 2 quotes, colon, and comma. */
        requestLength += STATUS_DETAILS_KEY_LENGTH + 4;
        requestLength += pUpdateInfo->statusDetailsLength;
    }

    /* Add the expected version if provided. */
    if( pUpdateInfo->expectedVersion != AWS_IOT_JOBS_NO_VERSION )
    {
        /* Convert the expected version to a string. */
        expectedVersionLength = snprintf( pExpectedVersion,
                                          EXPECTED_VERSION_STRING_LENGTH,
                                          "%u",
                                          pUpdateInfo->expectedVersion );
        AwsIotJobs_Assert( expectedVersionLength > 0 );
        AwsIotJobs_Assert( expectedVersionLength < EXPECTED_VERSION_STRING_LENGTH );

        /* Add 6 for the 4 quotes, colon, and comma. */
        requestLength += EXPECTED_VERSION_KEY_LENGTH + 6;
        requestLength += ( size_t ) expectedVersionLength;
    }

    /* Add the length of the execution number if present. */
    if( pUpdateInfo->executionNumber != AWS_IOT_JOBS_NO_EXECUTION_NUMBER )
    {
        /* Convert the execution number to a string. */
        executionNumberLength = snprintf( pExecutionNumber,
                                          EXECUTION_NUMBER_STRING_LENGTH,
                                          "%d",
                                          pUpdateInfo->executionNumber );
        AwsIotJobs_Assert( executionNumberLength > 0 );
        AwsIotJobs_Assert( executionNumberLength < EXECUTION_NUMBER_STRING_LENGTH );

        requestLength += EXECUTION_NUMBER_KEY_LENGTH + 4;
        requestLength += ( size_t ) executionNumberLength;
    }

    /* Add the flags if true. The default values are false, so the flags are not
     * needed if false. */
    if( pUpdateInfo->includeJobExecutionState == true )
    {
        /* Add the length of "includeJobExecutionState" plus 4 for 2 quotes, a colon,
         * and a comma. */
        requestLength += INCLUDE_JOB_EXECUTION_STATE_KEY_LENGTH + 4;

        /* Add the length of "true". */
        requestLength += 4;
    }

    if( pUpdateInfo->includeJobDocument == true )
    {
        /* Add the length of "includeJobDocument" plus 4 for 2 quotes, a colon,
         * and a comma. */
        requestLength += INCLUDE_JOB_DOCUMENT_KEY_LENGTH + 4;

        /* Add the length of "true". */
        requestLength += 4;
    }

    /* Add the step timeout if provided. */
    if( pUpdateInfo->stepTimeoutInMinutes != AWS_IOT_JOBS_NO_TIMEOUT )
    {
        /* Calculate the length of the step timeout. Add 4 for the 2 quotes, colon, and comma. */
        requestLength += STEP_TIMEOUT_KEY_LENGTH + 4;

        if( pUpdateInfo->stepTimeoutInMinutes == AWS_IOT_JOBS_CANCEL_TIMEOUT )
        {
            /* Step timeout will be set to -1. */
            pStepTimeout[ 0 ] = '-';
            pStepTimeout[ 1 ] = '1';
            stepTimeoutLength = 2;
        }
        else
        {
            /* Convert the step timeout to a string. */
            stepTimeoutLength = snprintf( pStepTimeout,
                                          STEP_TIMEOUT_STRING_LENGTH,
                                          "%d",
                                          pUpdateInfo->stepTimeoutInMinutes );
            AwsIotJobs_Assert( stepTimeoutLength > 0 );
            AwsIotJobs_Assert( stepTimeoutLength < STEP_TIMEOUT_STRING_LENGTH );
        }

        requestLength += ( size_t ) stepTimeoutLength;
    }

    /* Add the length of the client token. */
    if( pRequestInfo->pClientToken != AWS_IOT_JOBS_CLIENT_TOKEN_AUTOGENERATE )
    {
        AwsIotJobs_Assert( pRequestInfo->clientTokenLength > 0 );

        requestLength += pRequestInfo->clientTokenLength;
    }
    else
    {
        requestLength += CLIENT_TOKEN_AUTOGENERATE_LENGTH;
    }

    /* Allocate memory for the request JSON. */
    pJobsRequest = AwsIotJobs_MallocString( requestLength );

    if( pJobsRequest == NULL )
    {
        IotLogError( "No memory for Jobs UPDATE request." );
        status = AWS_IOT_JOBS_NO_MEMORY;
    }
    else
    {
        /* Clear the request JSON. */
        ( void ) memset( pJobsRequest, 0x00, requestLength );

        /* Construct the request JSON. */
        APPEND_STRING( pJobsRequest, copyOffset, "{\"", 2 );

        /* Add the status. */
        APPEND_STRING( pJobsRequest, copyOffset, STATUS_KEY, STATUS_KEY_LENGTH );
        APPEND_STRING( pJobsRequest, copyOffset, "\":\"", 3 );
        APPEND_STRING( pJobsRequest, copyOffset, pStatus, statusLength );
        APPEND_STRING( pJobsRequest, copyOffset, "\",\"", 3 );

        /* Add status details if present. */
        if( pUpdateInfo->pStatusDetails != AWS_IOT_JOBS_NO_STATUS_DETAILS )
        {
            copyOffset = _appendStatusDetails( pJobsRequest,
                                               copyOffset,
                                               pUpdateInfo->pStatusDetails,
                                               pUpdateInfo->statusDetailsLength );
        }

        /* Add expected version. */
        if( pUpdateInfo->expectedVersion != AWS_IOT_JOBS_NO_VERSION )
        {
            APPEND_STRING( pJobsRequest,
                           copyOffset,
                           EXPECTED_VERSION_KEY,
                           EXPECTED_VERSION_KEY_LENGTH );
            APPEND_STRING( pJobsRequest, copyOffset, "\":\"", 3 );
            APPEND_STRING( pJobsRequest, copyOffset, pExpectedVersion, expectedVersionLength );
            APPEND_STRING( pJobsRequest, copyOffset, "\",\"", 3 );
        }

        /* Add execution number. */
        if( pUpdateInfo->executionNumber != AWS_IOT_JOBS_NO_EXECUTION_NUMBER )
        {
            copyOffset = _appendExecutionNumber( pJobsRequest,
                                                 copyOffset,
                                                 pExecutionNumber,
                                                 executionNumberLength );
        }

        /* Add flags if not default values. */
        if( pUpdateInfo->includeJobExecutionState == true )
        {
            copyOffset = _appendFlag( pJobsRequest,
                                      copyOffset,
                                      INCLUDE_JOB_EXECUTION_STATE_KEY,
                                      INCLUDE_JOB_EXECUTION_STATE_KEY_LENGTH,
                                      true );
        }

        if( pUpdateInfo->includeJobDocument == true )
        {
            copyOffset = _appendFlag( pJobsRequest,
                                      copyOffset,
                                      INCLUDE_JOB_DOCUMENT_KEY,
                                      INCLUDE_JOB_DOCUMENT_KEY_LENGTH,
                                      true );
        }

        /* Add step timeout if provided. */
        if( pUpdateInfo->stepTimeoutInMinutes != AWS_IOT_JOBS_NO_TIMEOUT )
        {
            copyOffset = _appendStepTimeout( pJobsRequest,
                                             copyOffset,
                                             pStepTimeout,
                                             stepTimeoutLength );
        }

        /* Add the client token. */
        copyOffset = _appendClientToken( pJobsRequest, copyOffset, pRequestInfo, pOperation );

        APPEND_STRING( pJobsRequest, copyOffset, "\"}", 2 );

        /* Set the output parameters. */
        pOperation->pJobsRequest = pJobsRequest;
        pOperation->jobsRequestLength = requestLength;

        /* Ensure offsets are valid. */
        AwsIotJobs_Assert( copyOffset == requestLength );
        AwsIotJobs_Assert( pOperation->pClientToken > pOperation->pJobsRequest );
        AwsIotJobs_Assert( pOperation->pClientToken <
                           pOperation->pJobsRequest + pOperation->jobsRequestLength );

        IotLogDebug( "Jobs UPDATE request: %.*s",
                     pOperation->jobsRequestLength,
                     pOperation->pJobsRequest );
    }

    return status;
}

/*-----------------------------------------------------------*/

static AwsIotJobsError_t _parseErrorDocument( const char * pErrorDocument,
                                              size_t errorDocumentLength )
{
    AwsIotJobsError_t status = AWS_IOT_JOBS_BAD_RESPONSE;
    const char * pCode = NULL;
    size_t codeLength = 0;

    /* Find the error code. */
    if( AwsIotDocParser_FindValue( pErrorDocument,
                                   errorDocumentLength,
                                   CODE_KEY,
                                   CODE_KEY_LENGTH,
                                   &pCode,
                                   &codeLength ) == true )
    {
        switch( codeLength )
        {
            /* InvalidJson */
            case 13:

                if( strncmp( "\"InvalidJson\"", pCode, codeLength ) == 0 )
                {
                    status = AWS_IOT_JOBS_INVALID_JSON;
                }

                break;

            /* InvalidTopic */
            case 14:

                if( strncmp( "\"InvalidTopic\"", pCode, codeLength ) == 0 )
                {
                    status = AWS_IOT_JOBS_INVALID_TOPIC;
                }

                break;

            /* InternalError */
            case 15:

                if( strncmp( "\"InternalError\"", pCode, codeLength ) == 0 )
                {
                    status = AWS_IOT_JOBS_INTERNAL_ERROR;
                }

                break;

            /* InvalidRequest */
            case 16:

                if( strncmp( "\"InvalidRequest\"", pCode, codeLength ) == 0 )
                {
                    status = AWS_IOT_JOBS_INVALID_REQUEST;
                }

                break;

            /* VersionMismatch */
            case 17:

                if( strncmp( "\"VersionMismatch\"", pCode, codeLength ) == 0 )
                {
                    status = AWS_IOT_JOBS_VERSION_MISMATCH;
                }

                break;

            /* ResourceNotFound, RequestThrottled */
            case 18:

                if( strncmp( "\"ResourceNotFound\"", pCode, codeLength ) == 0 )
                {
                    status = AWS_IOT_JOBS_NOT_FOUND;
                }
                else if( strncmp( "\"RequestThrottled\"", pCode, codeLength ) == 0 )
                {
                    status = AWS_IOT_JOBS_THROTTLED;
                }

                break;

            /* TerminalStateReached */
            case 22:

                if( strncmp( "\"TerminalStateReached\"", pCode, codeLength ) == 0 )
                {
                    status = AWS_IOT_JOBS_TERMINAL_STATE;
                }

                break;

            /* InvalidStateTransition */
            case 24:

                if( strncmp( "\"InvalidStateTransition\"", pCode, codeLength ) == 0 )
                {
                    status = AWS_IOT_JOBS_INVALID_STATE;
                }

                break;

            default:

                /* Assume bad response status unless matched.*/
                break;
        }
    }

    return status;
}

/*-----------------------------------------------------------*/

AwsIotJobsError_t _AwsIotJobs_GenerateJsonRequest( _jobsOperationType_t type,
                                                   const AwsIotJobsRequestInfo_t * pRequestInfo,
                                                   const _jsonRequestContents_t * pRequestContents,
                                                   _jobsOperation_t * pOperation )
{
    AwsIotJobsError_t status = AWS_IOT_JOBS_STATUS_PENDING;

    /* Generate request based on the Job operation type. */
    switch( type )
    {
        case JOBS_GET_PENDING:
            status = _generateGetPendingRequest( pRequestInfo, pOperation );
            break;

        case JOBS_START_NEXT:
            status = _generateStartNextRequest( pRequestInfo,
                                                pRequestContents->pUpdateInfo,
                                                pOperation );
            break;

        case JOBS_DESCRIBE:
            status = _generateDescribeRequest( pRequestInfo,
                                               pRequestContents->describe.executionNumber,
                                               pRequestContents->describe.includeJobDocument,
                                               pOperation );
            break;

        default:
            /* The only remaining valid type is UPDATE. */
            AwsIotJobs_Assert( type == JOBS_UPDATE );

            status = _generateUpdateRequest( pRequestInfo,
                                             pRequestContents->pUpdateInfo,
                                             pOperation );
            break;
    }

    return status;
}

/*-----------------------------------------------------------*/

void _AwsIotJobs_ParseResponse( AwsIotStatus_t status,
                                const char * pResponse,
                                size_t responseLength,
                                _jobsOperation_t * pOperation )
{
    AwsIotJobs_Assert( pOperation->status == AWS_IOT_JOBS_STATUS_PENDING );

    /* A non-waitable operation can re-use the pointers from the publish info,
     * since those are guaranteed to be in-scope throughout the user callback.
     * But a waitable operation must copy the data from the publish info because
     * AwsIotJobs_Wait may be called after the MQTT library frees the publish
     * info. */
    if( ( pOperation->flags & AWS_IOT_JOBS_FLAG_WAITABLE ) == 0 )
    {
        pOperation->pJobsResponse = pResponse;
        pOperation->jobsResponseLength = responseLength;
    }
    else
    {
        IotLogDebug( "Allocating new buffer for waitable Jobs %s.",
                     _pAwsIotJobsOperationNames[ pOperation->type ] );

        /* Parameter validation should not have allowed a NULL malloc function. */
        AwsIotJobs_Assert( pOperation->mallocResponse != NULL );

        /* Allocate a buffer for the retrieved document. */
        pOperation->pJobsResponse = pOperation->mallocResponse( responseLength );

        if( pOperation->pJobsResponse == NULL )
        {
            IotLogError( "Failed to allocate buffer for retrieved Jobs %s response.",
                         _pAwsIotJobsOperationNames[ pOperation->type ] );

            pOperation->status = AWS_IOT_JOBS_NO_MEMORY;
        }
        else
        {
            /* Copy the response. */
            ( void ) memcpy( ( void * ) pOperation->pJobsResponse, pResponse, responseLength );
            pOperation->jobsResponseLength = responseLength;
        }
    }

    /* Set the status of the Jobs operation. */
    if( pOperation->status == AWS_IOT_JOBS_STATUS_PENDING )
    {
        if( status == AWS_IOT_ACCEPTED )
        {
            pOperation->status = AWS_IOT_JOBS_SUCCESS;
        }
        else
        {
            pOperation->status = _parseErrorDocument( pResponse, responseLength );
        }
    }
}

/*-----------------------------------------------------------*/
