/*
 * 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_api.c
 * @brief Implements the user-facing functions of the Jobs library.
 */

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

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

/* Platform threads include. */
#include "platform/iot_threads.h"

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

/* Error handling include. */
#include "iot_error.h"

/* MQTT include. */
#include "iot_mqtt.h"

/* Validate Jobs configuration settings. */
#if AWS_IOT_JOBS_ENABLE_ASSERTS != 0 && AWS_IOT_JOBS_ENABLE_ASSERTS != 1
    #error "AWS_IOT_JOBS_ENABLE_ASSERTS must be 0 or 1."
#endif
#if AWS_IOT_JOBS_DEFAULT_MQTT_TIMEOUT_MS <= 0
    #error "AWS_IOT_JOBS_DEFAULT_MQTT_TIMEOUT_MS cannot be 0 or negative."
#endif
#if AWS_IOT_JOBS_NOTIFY_CALLBACKS <= 0
    #error "AWS_IOT_JOBS_NOTIFY_CALLBACKS cannot be 0 or negative."
#endif

/**
 * @brief Returned by @ref _getCallbackIndex when there's no space in the callback array.
 */
#define NO_SPACE_FOR_CALLBACK     ( -1 )

/**
 * @brief Returned by @ref _getCallbackIndex when a searching for an oldCallback that
 * does not exist.
 */
#define OLD_CALLBACK_NOT_FOUND    ( -2 )

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

/**
 * @brief Check if the library is initialized.
 *
 * @return `true` if AwsIotJobs_Init was called; `false` otherwise.
 */
static bool _checkInit( void );

/**
 * @brief Validate the #AwsIotJobsRequestInfo_t passed to a Jobs API function.
 *
 * @param[in] type The Jobs API function type.
 * @param[in] pRequestInfo The request info passed to a Jobs API function.
 * @param[in] flags Flags used by the Jobs API function.
 * @param[in] pCallbackInfo The callback info passed with the request info.
 * @param[in] pOperation Operation reference pointer passed to a Jobs API function.
 *
 * @return #AWS_IOT_JOBS_SUCCESS or #AWS_IOT_JOBS_BAD_PARAMETER.
 */
static AwsIotJobsError_t _validateRequestInfo( _jobsOperationType_t type,
                                               const AwsIotJobsRequestInfo_t * pRequestInfo,
                                               uint32_t flags,
                                               const AwsIotJobsCallbackInfo_t * pCallbackInfo,
                                               const AwsIotJobsOperation_t * pOperation );

/**
 * @brief Validate the #AwsIotJobsUpdateInfo_t passed to a Jobs API function.
 *
 * @param[in] type The Jobs API function type.
 * @param[in] pUpdateInfo The update info passed to a Jobs API function.
 *
 * @return #AWS_IOT_JOBS_SUCCESS or #AWS_IOT_JOBS_BAD_PARAMETER.
 */
static AwsIotJobsError_t _validateUpdateInfo( _jobsOperationType_t type,
                                              const AwsIotJobsUpdateInfo_t * pUpdateInfo );

/**
 * @brief Gets an element of the callback array to modify.
 *
 * @param[in] type The type of callback to modify.
 * @param[in] pSubscription Subscription object that holds the callback array.
 * @param[in] pCallbackInfo User provided callback info.
 *
 * @return The index of the callback array to modify; on error:
 * - #NO_SPACE_FOR_CALLBACK if no free spaces are available
 * - #OLD_CALLBACK_NOT_FOUND if an old callback to remove was specified, but that function does not exist.
 *
 * @note This function should be called with the subscription list mutex locked.
 */
static int32_t _getCallbackIndex( _jobsCallbackType_t type,
                                  _jobsSubscription_t * pSubscription,
                                  const AwsIotJobsCallbackInfo_t * pCallbackInfo );

/**
 * @brief Common function for setting Jobs callbacks.
 *
 * @param[in] mqttConnection The MQTT connection to use.
 * @param[in] type Type of Jobs callback.
 * @param[in] pThingName Thing Name for Jobs callback.
 * @param[in] thingNameLength Length of `pThingName`.
 * @param[in] pCallbackInfo Callback information to set.
 *
 * @return #AWS_IOT_JOBS_SUCCESS, #AWS_IOT_JOBS_BAD_PARAMETER,
 * #AWS_IOT_JOBS_NO_MEMORY, or #AWS_IOT_JOBS_MQTT_ERROR.
 */
static AwsIotJobsError_t _setCallbackCommon( IotMqttConnection_t mqttConnection,
                                             _jobsCallbackType_t type,
                                             const char * pThingName,
                                             size_t thingNameLength,
                                             const AwsIotJobsCallbackInfo_t * pCallbackInfo );

/**
 * @brief Change the subscriptions for Jobs callbacks, either by subscribing
 * or unsubscribing.
 *
 * @param[in] mqttConnection The MQTT connection to use.
 * @param[in] type Type of Jobs callback.
 * @param[in] pSubscription Jobs subscriptions object for callback.
 * @param[in] mqttOperation Either @ref mqtt_function_subscribesync or
 * @ref mqtt_function_unsubscribesync.
 *
 * @return #AWS_IOT_JOBS_SUCCESS, #AWS_IOT_JOBS_NO_MEMORY, or
 * #AWS_IOT_JOBS_MQTT_ERROR.
 */
static AwsIotJobsError_t _modifyCallbackSubscriptions( IotMqttConnection_t mqttConnection,
                                                       _jobsCallbackType_t type,
                                                       _jobsSubscription_t * pSubscription,
                                                       AwsIotMqttFunction_t mqttOperation );

/**
 * @brief Invoked when a document is received on the Jobs NOTIFY PENDING callback.
 *
 * @param[in] pArgument Ignored.
 * @param[in] pMessage The received Jobs document (as an MQTT PUBLISH message).
 */
static void _notifyPendingCallbackWrapper( void * pArgument,
                                           IotMqttCallbackParam_t * pMessage );

/**
 * @brief Invoked when a document is received on the Jobs NOTIFY NEXT callback.
 *
 * @param[in] pArgument Ignored.
 * @param[in] pMessage The received Jobs document (as an MQTT PUBLISH message).
 */
static void _notifyNextCallbackWrapper( void * pArgument,
                                        IotMqttCallbackParam_t * pMessage );

/**
 * @brief Common function for incoming Jobs callbacks.
 *
 * @param[in] type Jobs callback type.
 * @param[in] pMessage The received Jobs callback document (as an MQTT PUBLISH
 * message).
 */
static void _callbackWrapperCommon( _jobsCallbackType_t type,
                                    IotMqttCallbackParam_t * pMessage );

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

/**
 * @brief Tracks whether @ref jobs_function_init has been called.
 *
 * API functions will fail if @ref jobs_function_init was not called.
 */
static uint32_t _initCalled = 0;

/**
 * @brief Timeout used for MQTT operations.
 */
uint32_t _AwsIotJobsMqttTimeoutMs = AWS_IOT_JOBS_DEFAULT_MQTT_TIMEOUT_MS;

#if LIBRARY_LOG_LEVEL > IOT_LOG_NONE

/**
 * @brief Printable names for the Jobs callbacks.
 */
    const char * const _pAwsIotJobsCallbackNames[] =
    {
        "NOTIFY PENDING",
        "NOTIFY NEXT"
    };
#endif

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

static bool _checkInit( void )
{
    bool status = true;

    if( _initCalled == 0 )
    {
        IotLogError( "AwsIotJobs_Init was not called." );

        status = false;
    }

    return status;
}

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

static AwsIotJobsError_t _validateRequestInfo( _jobsOperationType_t type,
                                               const AwsIotJobsRequestInfo_t * pRequestInfo,
                                               uint32_t flags,
                                               const AwsIotJobsCallbackInfo_t * pCallbackInfo,
                                               const AwsIotJobsOperation_t * pOperation )
{
    IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_SUCCESS );

    /* Check that the given MQTT connection is valid. */
    if( pRequestInfo->mqttConnection == IOT_MQTT_CONNECTION_INITIALIZER )
    {
        IotLogError( "MQTT connection is not initialized for Jobs %s.",
                     _pAwsIotJobsOperationNames[ type ] );

        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
    }

    /* Check Thing Name. */
    if( AwsIot_ValidateThingName( pRequestInfo->pThingName,
                                  pRequestInfo->thingNameLength ) == false )
    {
        IotLogError( "Thing Name is not valid for Jobs %s.",
                     _pAwsIotJobsOperationNames[ type ] );

        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
    }

    /* Checks for waitable operations. */
    if( ( flags & AWS_IOT_JOBS_FLAG_WAITABLE ) == AWS_IOT_JOBS_FLAG_WAITABLE )
    {
        if( pOperation == NULL )
        {
            IotLogError( "Reference must be provided for a waitable Jobs %s.",
                         _pAwsIotJobsOperationNames[ type ] );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }

        if( pRequestInfo->mallocResponse == NULL )
        {
            IotLogError( "Memory allocation function must be set for waitable Jobs %s.",
                         _pAwsIotJobsOperationNames[ type ] );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }

        if( pCallbackInfo != NULL )
        {
            IotLogError( "Callback should not be set for a waitable Jobs %s.",
                         _pAwsIotJobsOperationNames[ type ] );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }
    }

    /* Check that a callback function is set. */
    if( pCallbackInfo != NULL )
    {
        if( pCallbackInfo->function == NULL )
        {
            IotLogError( "Callback function must be set for Jobs %s callback.",
                         _pAwsIotJobsOperationNames[ type ] );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }
    }

    /* Check client token length. */
    if( pRequestInfo->pClientToken != AWS_IOT_JOBS_CLIENT_TOKEN_AUTOGENERATE )
    {
        if( pRequestInfo->clientTokenLength == 0 )
        {
            IotLogError( "Client token length must be set for Jobs %s.",
                         _pAwsIotJobsOperationNames[ type ] );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }

        if( pRequestInfo->clientTokenLength > AWS_IOT_CLIENT_TOKEN_MAX_LENGTH )
        {
            IotLogError( "Client token for Jobs %s cannot be longer than %d.",
                         _pAwsIotJobsOperationNames[ type ],
                         AWS_IOT_CLIENT_TOKEN_MAX_LENGTH );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }
    }

    /* Check Job ID for DESCRIBE and UPDATE. */
    if( ( type == JOBS_DESCRIBE ) || ( type == JOBS_UPDATE ) )
    {
        if( ( pRequestInfo->pJobId == NULL ) || ( pRequestInfo->jobIdLength == 0 ) )
        {
            IotLogError( "Job ID must be set for Jobs %s.",
                         _pAwsIotJobsOperationNames[ type ] );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }

        if( pRequestInfo->jobIdLength > JOBS_MAX_ID_LENGTH )
        {
            IotLogError( "Job ID for Jobs %s cannot be longer than %d.",
                         _pAwsIotJobsOperationNames[ type ],
                         JOBS_MAX_ID_LENGTH );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }
    }

    /* A Job ID (not $next job) must be specified for UPDATE. */
    if( type == JOBS_UPDATE )
    {
        if( ( pRequestInfo->jobIdLength == AWS_IOT_JOBS_NEXT_JOB_LENGTH ) &&
            ( strncmp( AWS_IOT_JOBS_NEXT_JOB,
                       pRequestInfo->pJobId,
                       AWS_IOT_JOBS_NEXT_JOB_LENGTH ) == 0 ) )
        {
            IotLogError( "Job ID $next is not valid for Jobs UPDATE." );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }
    }

    IOT_FUNCTION_EXIT_NO_CLEANUP();
}

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

static AwsIotJobsError_t _validateUpdateInfo( _jobsOperationType_t type,
                                              const AwsIotJobsUpdateInfo_t * pUpdateInfo )
{
    IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_SUCCESS );

    /* Only START NEXT and UPDATE operations need an update info. */
    AwsIotJobs_Assert( ( type == JOBS_START_NEXT ) || ( type == JOBS_UPDATE ) );

    /* Check that Job status to report is valid for Jobs UPDATE. */
    if( type == JOBS_UPDATE )
    {
        switch( pUpdateInfo->newStatus )
        {
            case AWS_IOT_JOB_STATE_IN_PROGRESS:
            case AWS_IOT_JOB_STATE_FAILED:
            case AWS_IOT_JOB_STATE_SUCCEEDED:
            case AWS_IOT_JOB_STATE_REJECTED:
                break;

            default:
                IotLogError( "Job state %s is not valid for Jobs UPDATE.",
                             AwsIotJobs_StateName( pUpdateInfo->newStatus ) );

                IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }
    }

    /* Check that step timeout is valid. */
    if( ( pUpdateInfo->stepTimeoutInMinutes != AWS_IOT_JOBS_NO_TIMEOUT ) &&
        ( pUpdateInfo->stepTimeoutInMinutes != AWS_IOT_JOBS_CANCEL_TIMEOUT ) )
    {
        if( pUpdateInfo->stepTimeoutInMinutes < 1 )
        {
            IotLogError( "Step timeout for Jobs %s must be at least 1.",
                         _pAwsIotJobsOperationNames[ type ] );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }
        else if( pUpdateInfo->stepTimeoutInMinutes > JOBS_MAX_TIMEOUT )
        {
            IotLogError( "Step timeout for Jobs %s cannot exceed %d.",
                         _pAwsIotJobsOperationNames[ type ],
                         JOBS_MAX_TIMEOUT );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }
    }

    /* Check status details. */
    if( pUpdateInfo->pStatusDetails != AWS_IOT_JOBS_NO_STATUS_DETAILS )
    {
        if( pUpdateInfo->statusDetailsLength == 0 )
        {
            IotLogError( "Status details length not set for Jobs %s.",
                         _pAwsIotJobsOperationNames[ type ] );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }

        if( pUpdateInfo->statusDetailsLength > JOBS_MAX_STATUS_DETAILS_LENGTH )
        {
            IotLogError( "Status details length for Jobs %s cannot exceed %d.",
                         _pAwsIotJobsOperationNames[ type ],
                         JOBS_MAX_STATUS_DETAILS_LENGTH );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }
    }

    IOT_FUNCTION_EXIT_NO_CLEANUP();
}

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

static int32_t _getCallbackIndex( _jobsCallbackType_t type,
                                  _jobsSubscription_t * pSubscription,
                                  const AwsIotJobsCallbackInfo_t * pCallbackInfo )
{
    int32_t callbackIndex = 0;

    /* Find the matching oldFunction. */
    if( pCallbackInfo->oldFunction != NULL )
    {
        for( callbackIndex = 0; callbackIndex < AWS_IOT_JOBS_NOTIFY_CALLBACKS; callbackIndex++ )
        {
            if( pSubscription->callbacks[ type ][ callbackIndex ].function == pCallbackInfo->oldFunction )
            {
                /* oldFunction found. */
                break;
            }
        }

        if( callbackIndex == AWS_IOT_JOBS_NOTIFY_CALLBACKS )
        {
            /* oldFunction not found. */
            callbackIndex = OLD_CALLBACK_NOT_FOUND;
        }
    }
    /* Find space for a new callback. */
    else
    {
        for( callbackIndex = 0; callbackIndex < AWS_IOT_JOBS_NOTIFY_CALLBACKS; callbackIndex++ )
        {
            if( pSubscription->callbacks[ type ][ callbackIndex ].function == NULL )
            {
                break;
            }
        }

        if( callbackIndex == AWS_IOT_JOBS_NOTIFY_CALLBACKS )
        {
            /* No memory for new callback. */
            callbackIndex = NO_SPACE_FOR_CALLBACK;
        }
    }

    return callbackIndex;
}

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

static AwsIotJobsError_t _setCallbackCommon( IotMqttConnection_t mqttConnection,
                                             _jobsCallbackType_t type,
                                             const char * pThingName,
                                             size_t thingNameLength,
                                             const AwsIotJobsCallbackInfo_t * pCallbackInfo )
{
    IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_SUCCESS );
    bool subscriptionMutexLocked = false;
    _jobsSubscription_t * pSubscription = NULL;
    int32_t callbackIndex = 0;

    /* Check that AwsIotJobs_Init was called. */
    if( _checkInit() == false )
    {
        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NOT_INITIALIZED );
    }

    /* Validate Thing Name. */
    if( AwsIot_ValidateThingName( pThingName, thingNameLength ) == false )
    {
        IotLogError( "Thing Name for Jobs %s callback is not valid.",
                     _pAwsIotJobsCallbackNames[ type ] );

        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
    }

    /* Check that a callback parameter is provided. */
    if( pCallbackInfo == NULL )
    {
        IotLogError( "Callback parameter must be provided for Jobs %s callback.",
                     _pAwsIotJobsCallbackNames[ type ] );

        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
    }

    /* The oldFunction member must be set when removing or replacing a callback. */
    if( pCallbackInfo->function == NULL )
    {
        if( pCallbackInfo->oldFunction == NULL )
        {
            IotLogError( "Both oldFunction and function pointers cannot be NULL for Jobs %s callback.",
                         _pAwsIotJobsCallbackNames[ type ] );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
        }
    }

    IotLogInfo( "(%.*s) Modifying Jobs %s callback.",
                thingNameLength,
                pThingName,
                _pAwsIotJobsCallbackNames[ type ] );

    /* Lock the subscription list mutex to check for an existing subscription
     * object. */
    IotMutex_Lock( &_AwsIotJobsSubscriptionsMutex );
    subscriptionMutexLocked = true;

    /* Check for an existing subscription. This function will attempt to allocate
     * a new subscription if not found. */
    pSubscription = _AwsIotJobs_FindSubscription( pThingName, thingNameLength, true );

    if( pSubscription == NULL )
    {
        /* No existing subscription was found, and no new subscription could be
         * allocated. */
        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NO_MEMORY );
    }

    /* Get the index of the callback element to modify. */
    callbackIndex = _getCallbackIndex( type, pSubscription, pCallbackInfo );

    switch( callbackIndex )
    {
        case NO_SPACE_FOR_CALLBACK:
            IotLogError( "No memory for a new Jobs %s callback.",
                         _pAwsIotJobsCallbackNames[ type ] );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NO_MEMORY );

        case OLD_CALLBACK_NOT_FOUND:
            IotLogWarn( "Requested replacement function for Jobs %s callback not found.",
                        _pAwsIotJobsCallbackNames[ type ] );

            /* A subscription may have been allocated, but the callback operation can't
             * proceed. Check if the subscription should be removed. */
            _AwsIotJobs_RemoveSubscription( pSubscription, NULL );

            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );

        default:
            break;
    }

    /* Check for an existing callback. */
    if( pSubscription->callbacks[ type ][ callbackIndex ].function != NULL )
    {
        /* Replace existing callback. */
        if( pCallbackInfo->function != NULL )
        {
            IotLogInfo( "(%.*s) Found existing %s callback. Replacing callback.",
                        thingNameLength,
                        pThingName,
                        _pAwsIotJobsCallbackNames[ type ] );

            pSubscription->callbacks[ type ][ callbackIndex ] = *pCallbackInfo;
        }
        /* Remove existing callback. */
        else
        {
            IotLogInfo( "(%.*s) Removing existing %s callback.",
                        thingNameLength,
                        pThingName,
                        _pAwsIotJobsCallbackNames[ type ] );

            /* Clear the callback information and unsubscribe. */
            ( void ) memset( &( pSubscription->callbacks[ type ][ callbackIndex ] ),
                             0x00,
                             sizeof( AwsIotJobsCallbackInfo_t ) );
            ( void ) _modifyCallbackSubscriptions( mqttConnection,
                                                   type,
                                                   pSubscription,
                                                   IotMqtt_UnsubscribeSync );

            /* Check if this subscription object can be removed. */
            _AwsIotJobs_RemoveSubscription( pSubscription, NULL );
        }
    }
    /* No existing callback. */
    else
    {
        /* Add new callback. */
        IotLogInfo( "(%.*s) Adding new %s callback.",
                    thingNameLength,
                    pThingName,
                    _pAwsIotJobsCallbackNames[ type ] );

        status = _modifyCallbackSubscriptions( mqttConnection,
                                               type,
                                               pSubscription,
                                               IotMqtt_SubscribeSync );

        if( status == AWS_IOT_JOBS_SUCCESS )
        {
            pSubscription->callbacks[ type ][ callbackIndex ] = *pCallbackInfo;
        }
        else
        {
            /* On failure, check if this subscription can be removed. */
            _AwsIotJobs_RemoveSubscription( pSubscription, NULL );
        }
    }

    IOT_FUNCTION_CLEANUP_BEGIN();

    if( subscriptionMutexLocked == true )
    {
        IotMutex_Unlock( &_AwsIotJobsSubscriptionsMutex );
    }

    IotLogInfo( "(%.*s) Jobs %s callback operation complete with result %s.",
                thingNameLength,
                pThingName,
                _pAwsIotJobsCallbackNames[ type ],
                AwsIotJobs_strerror( status ) );

    IOT_FUNCTION_CLEANUP_END();
}

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

static AwsIotJobsError_t _modifyCallbackSubscriptions( IotMqttConnection_t mqttConnection,
                                                       _jobsCallbackType_t type,
                                                       _jobsSubscription_t * pSubscription,
                                                       AwsIotMqttFunction_t mqttOperation )
{
    IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_SUCCESS );
    int32_t i = 0;
    IotMqttError_t mqttStatus = IOT_MQTT_STATUS_PENDING;
    IotMqttSubscription_t subscription = IOT_MQTT_SUBSCRIPTION_INITIALIZER;
    AwsIotTopicInfo_t topicInfo = { 0 };
    char * pTopicFilter = NULL;
    uint16_t topicFilterLength = 0;

    /* Lookup table for Jobs callback suffixes. */
    const char * const pCallbackSuffix[ JOBS_CALLBACK_COUNT ] =
    {
        JOBS_NOTIFY_PENDING_CALLBACK_STRING, /* Notify pending callback. */
        JOBS_NOTIFY_NEXT_CALLBACK_STRING     /* Notify next callback. */
    };

    /* Lookup table for Jobs callback suffix lengths. */
    const uint16_t pCallbackSuffixLength[ JOBS_CALLBACK_COUNT ] =
    {
        JOBS_NOTIFY_PENDING_CALLBACK_STRING_LENGTH, /* Notify pending callback. */
        JOBS_NOTIFY_NEXT_CALLBACK_STRING_LENGTH     /* Notify next callback. */
    };

    /* Lookup table for Jobs callback function wrappers. */
    const AwsIotMqttCallbackFunction_t pCallbackWrapper[ JOBS_CALLBACK_COUNT ] =
    {
        _notifyPendingCallbackWrapper, /* Notify pending callback. */
        _notifyNextCallbackWrapper,    /* Notify next callback. */
    };

    /* MQTT operation may only be subscribe or unsubscribe. */
    AwsIotJobs_Assert( ( mqttOperation == IotMqtt_SubscribeSync ) ||
                       ( mqttOperation == IotMqtt_UnsubscribeSync ) );

    /* Check if any subscriptions are currently registered for this type. */
    for( i = 0; i < AWS_IOT_JOBS_NOTIFY_CALLBACKS; i++ )
    {
        if( pSubscription->callbacks[ type ][ i ].function != NULL )
        {
            /* No action is needed when another callback exists. */
            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_SUCCESS );
        }
    }

    /* Use the subscription's topic buffer if available. */
    if( pSubscription->pTopicBuffer != NULL )
    {
        pTopicFilter = pSubscription->pTopicBuffer;
    }

    /* Generate the topic for the MQTT subscription. */
    topicInfo.pThingName = pSubscription->pThingName;
    topicInfo.thingNameLength = pSubscription->thingNameLength;
    topicInfo.longestSuffixLength = JOBS_LONGEST_SUFFIX_LENGTH;
    topicInfo.mallocString = AwsIotJobs_MallocString;
    topicInfo.pOperationName = pCallbackSuffix[ type ];
    topicInfo.operationNameLength = pCallbackSuffixLength[ type ];

    if( AwsIot_GenerateOperationTopic( &topicInfo,
                                       &pTopicFilter,
                                       &topicFilterLength ) == false )
    {
        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NO_MEMORY );
    }

    IotLogDebug( "%s subscription for %.*s",
                 mqttOperation == IotMqtt_SubscribeSync ? "Adding" : "Removing",
                 topicFilterLength,
                 pTopicFilter );

    /* Set the members of the MQTT subscription. */
    subscription.qos = IOT_MQTT_QOS_1;
    subscription.pTopicFilter = pTopicFilter;
    subscription.topicFilterLength = topicFilterLength;
    subscription.callback.pCallbackContext = NULL;
    subscription.callback.function = pCallbackWrapper[ type ];

    /* Call the MQTT operation function. */
    mqttStatus = mqttOperation( mqttConnection,
                                &subscription,
                                1,
                                0,
                                _AwsIotJobsMqttTimeoutMs );

    /* Check the result of the MQTT operation. */
    if( mqttStatus != IOT_MQTT_SUCCESS )
    {
        IotLogError( "Failed to %s callback for %.*s %s callback, error %s.",
                     mqttOperation == IotMqtt_SubscribeSync ? "subscribe to" : "unsubscribe from",
                     pSubscription->thingNameLength,
                     pSubscription->pThingName,
                     _pAwsIotJobsCallbackNames[ type ],
                     IotMqtt_strerror( mqttStatus ) );

        /* Convert the MQTT "NO MEMORY" error to a Jobs "NO MEMORY" error. */
        if( mqttStatus == IOT_MQTT_NO_MEMORY )
        {
            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NO_MEMORY );
        }
        else
        {
            IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_MQTT_ERROR );
        }
    }

    IotLogDebug( "Successfully %s %.*s Jobs %s callback.",
                 mqttOperation == IotMqtt_SubscribeSync ? "subscribed to" : "unsubscribed from",
                 pSubscription->thingNameLength,
                 pSubscription->pThingName,
                 _pAwsIotJobsCallbackNames[ type ] );

    IOT_FUNCTION_CLEANUP_BEGIN();

    /* MQTT subscribe should check the subscription topic buffer. */
    if( mqttOperation == IotMqtt_SubscribeSync )
    {
        /* If the current subscription has no topic buffer, assign it the current
         * topic buffer. */
        if( pSubscription->pTopicBuffer == NULL )
        {
            pSubscription->pTopicBuffer = pTopicFilter;
        }
    }

    IOT_FUNCTION_CLEANUP_END();
}

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

static void _notifyPendingCallbackWrapper( void * pArgument,
                                           IotMqttCallbackParam_t * pMessage )
{
    /* Silence warnings about unused parameters. */
    ( void ) pArgument;

    _callbackWrapperCommon( NOTIFY_PENDING_CALLBACK,
                            pMessage );
}

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

static void _notifyNextCallbackWrapper( void * pArgument,
                                        IotMqttCallbackParam_t * pMessage )
{
    /* Silence warnings about unused parameters. */
    ( void ) pArgument;

    _callbackWrapperCommon( NOTIFY_NEXT_CALLBACK,
                            pMessage );
}

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

static void _callbackWrapperCommon( _jobsCallbackType_t type,
                                    IotMqttCallbackParam_t * pMessage )
{
    int32_t callbackIndex = 0;
    AwsIotJobsCallbackInfo_t callbackInfo = AWS_IOT_JOBS_CALLBACK_INFO_INITIALIZER;
    AwsIotJobsCallbackParam_t callbackParam = { .callbackType = ( AwsIotJobsCallbackType_t ) 0 };
    _jobsSubscription_t * pSubscription = NULL;
    const char * pThingName = NULL;
    size_t thingNameLength = 0;

    /* Parse the Thing Name from the topic. */
    if( AwsIot_ParseThingName( pMessage->u.message.info.pTopicName,
                               pMessage->u.message.info.topicNameLength,
                               &pThingName,
                               &thingNameLength ) == false )
    {
        IOT_GOTO_CLEANUP();
    }

    /* Search for a matching subscription. */
    IotMutex_Lock( &_AwsIotJobsSubscriptionsMutex );

    pSubscription = _AwsIotJobs_FindSubscription( pThingName,
                                                  thingNameLength,
                                                  false );

    if( pSubscription != NULL )
    {
        /* Increment callback reference count to prevent the subscription object from being
         * freed while callbacks are running. */
        pSubscription->callbackReferences++;
    }

    IotMutex_Unlock( &_AwsIotJobsSubscriptionsMutex );

    if( pSubscription != NULL )
    {
        /* Invoke all callbacks for this Thing. */
        for( callbackIndex = 0; callbackIndex < AWS_IOT_JOBS_NOTIFY_CALLBACKS; callbackIndex++ )
        {
            /* Copy the callback function and parameter, as it may be modified at any time. */
            IotMutex_Lock( &_AwsIotJobsSubscriptionsMutex );
            callbackInfo = pSubscription->callbacks[ type ][ callbackIndex ];
            IotMutex_Unlock( &_AwsIotJobsSubscriptionsMutex );

            if( callbackInfo.function != NULL )
            {
                /* Set the callback type. Jobs callbacks are enumerated after the operations. */
                callbackParam.callbackType = ( AwsIotJobsCallbackType_t ) ( type + JOBS_OPERATION_COUNT );

                /* Set the remaining members of the callback param. */
                callbackParam.mqttConnection = pMessage->mqttConnection;
                callbackParam.pThingName = pThingName;
                callbackParam.thingNameLength = thingNameLength;
                callbackParam.u.callback.pDocument = pMessage->u.message.info.pPayload;
                callbackParam.u.callback.documentLength = pMessage->u.message.info.payloadLength;

                /* Invoke the callback function. */
                callbackInfo.function( callbackInfo.pCallbackContext,
                                       &callbackParam );
            }
        }

        /* Callbacks are finished. Decrement reference count and check if subscription
         * object should be destroyed. */
        IotMutex_Lock( &_AwsIotJobsSubscriptionsMutex );

        pSubscription->callbackReferences--;
        AwsIotJobs_Assert( pSubscription->callbackReferences >= 0 );

        _AwsIotJobs_RemoveSubscription( pSubscription, NULL );

        IotMutex_Unlock( &_AwsIotJobsSubscriptionsMutex );
    }

    /* This function uses cleanup sections to exit on error. */
    IOT_FUNCTION_CLEANUP_BEGIN();
}

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

AwsIotJobsError_t AwsIotJobs_Init( uint32_t mqttTimeoutMs )
{
    AwsIotJobsError_t status = AWS_IOT_JOBS_SUCCESS;
    bool listInitStatus = false;

    if( _initCalled == 0 )
    {
        listInitStatus = AwsIot_InitLists( &_AwsIotJobsPendingOperations,
                                           &_AwsIotJobsSubscriptions,
                                           &_AwsIotJobsPendingOperationsMutex,
                                           &_AwsIotJobsSubscriptionsMutex );

        if( listInitStatus == false )
        {
            IotLogInfo( "Failed to create Jobs lists." );

            status = AWS_IOT_JOBS_INIT_FAILED;
        }
        else
        {
            /* Save the MQTT timeout option. */
            if( mqttTimeoutMs != 0 )
            {
                _AwsIotJobsMqttTimeoutMs = mqttTimeoutMs;
            }

            /* Set the flag that specifies initialization is complete. */
            _initCalled = 1;

            IotLogInfo( "Jobs library successfully initialized." );
        }
    }
    else
    {
        IotLogWarn( "AwsIotJobs_Init called with library already initialized." );
    }

    return status;
}

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

void AwsIotJobs_Cleanup( void )
{
    if( _initCalled == 1 )
    {
        _initCalled = 0;

        /* Remove and free all items in the Jobs pending operation list. */
        IotMutex_Lock( &_AwsIotJobsPendingOperationsMutex );
        IotListDouble_RemoveAll( &_AwsIotJobsPendingOperations,
                                 _AwsIotJobs_DestroyOperation,
                                 offsetof( _jobsOperation_t, link ) );
        IotMutex_Unlock( &_AwsIotJobsPendingOperationsMutex );

        /* Remove and free all items in the Jobs subscription list. */
        IotMutex_Lock( &_AwsIotJobsSubscriptionsMutex );
        IotListDouble_RemoveAll( &_AwsIotJobsSubscriptions,
                                 _AwsIotJobs_DestroySubscription,
                                 offsetof( _jobsSubscription_t, link ) );
        IotMutex_Unlock( &_AwsIotJobsSubscriptionsMutex );

        /* Restore the default MQTT timeout. */
        _AwsIotJobsMqttTimeoutMs = AWS_IOT_JOBS_DEFAULT_MQTT_TIMEOUT_MS;

        /* Destroy Jobs library mutexes. */
        IotMutex_Destroy( &_AwsIotJobsPendingOperationsMutex );
        IotMutex_Destroy( &_AwsIotJobsSubscriptionsMutex );

        IotLogInfo( "Jobs library cleanup done." );
    }
    else
    {
        IotLogWarn( "AwsIotJobs_Init was not called before AwsIotShadow_Cleanup." );
    }
}

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

AwsIotJobsError_t AwsIotJobs_GetPendingAsync( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                              uint32_t flags,
                                              const AwsIotJobsCallbackInfo_t * pCallbackInfo,
                                              AwsIotJobsOperation_t * const pGetPendingOperation )
{
    IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_STATUS_PENDING );
    _jobsOperation_t * pOperation = NULL;

    /* Check that AwsIotJobs_Init was called. */
    if( _checkInit() == false )
    {
        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NOT_INITIALIZED );
    }

    /* Check request info. */
    status = _validateRequestInfo( JOBS_GET_PENDING,
                                   pRequestInfo,
                                   flags,
                                   pCallbackInfo,
                                   pGetPendingOperation );

    if( status != AWS_IOT_JOBS_SUCCESS )
    {
        IOT_GOTO_CLEANUP();
    }

    /* Allocate a new Jobs operation. */
    status = _AwsIotJobs_CreateOperation( JOBS_GET_PENDING,
                                          pRequestInfo,
                                          NULL,
                                          flags,
                                          pCallbackInfo,
                                          &pOperation );

    if( status != AWS_IOT_JOBS_SUCCESS )
    {
        /* No memory for Jobs operation. */
        IOT_GOTO_CLEANUP();
    }

    /* Set the reference if provided. This must be done before the Jobs operation
     * is processed. */
    if( pGetPendingOperation != NULL )
    {
        *pGetPendingOperation = pOperation;
    }

    /* Process the Jobs operation. This subscribes to any required topics and
     * sends the MQTT message for the Jobs operation. */
    status = _AwsIotJobs_ProcessOperation( pRequestInfo, pOperation );

    /* If the Jobs operation failed, clear the now invalid reference. */
    if( ( status != AWS_IOT_JOBS_STATUS_PENDING ) && ( pGetPendingOperation != NULL ) )
    {
        *pGetPendingOperation = AWS_IOT_JOBS_OPERATION_INITIALIZER;
    }

    IOT_FUNCTION_EXIT_NO_CLEANUP();
}

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

AwsIotJobsError_t AwsIotJobs_GetPendingSync( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                             uint32_t flags,
                                             uint32_t timeoutMs,
                                             AwsIotJobsResponse_t * const pJobsResponse )
{
    AwsIotJobsError_t status = AWS_IOT_JOBS_STATUS_PENDING;
    AwsIotJobsOperation_t getPendingOperation = AWS_IOT_JOBS_OPERATION_INITIALIZER;

    /* Set the waitable flag. */
    flags |= AWS_IOT_JOBS_FLAG_WAITABLE;

    /* Call the asynchronous Jobs Get Pending function. */
    status = AwsIotJobs_GetPendingAsync( pRequestInfo,
                                         flags,
                                         NULL,
                                         &getPendingOperation );

    /* Wait for the Jobs Get Pending operation to complete. */
    if( status == AWS_IOT_JOBS_STATUS_PENDING )
    {
        status = AwsIotJobs_Wait( getPendingOperation,
                                  timeoutMs,
                                  pJobsResponse );
    }

    return status;
}

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

AwsIotJobsError_t AwsIotJobs_StartNextAsync( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                             const AwsIotJobsUpdateInfo_t * pUpdateInfo,
                                             uint32_t flags,
                                             const AwsIotJobsCallbackInfo_t * pCallbackInfo,
                                             AwsIotJobsOperation_t * const pStartNextOperation )
{
    IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_STATUS_PENDING );
    _jobsOperation_t * pOperation = NULL;
    _jsonRequestContents_t requestContents = { 0 };

    /* Check that AwsIotJobs_Init was called. */
    if( _checkInit() == false )
    {
        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NOT_INITIALIZED );
    }

    /* Check request info. */
    status = _validateRequestInfo( JOBS_START_NEXT,
                                   pRequestInfo,
                                   flags,
                                   pCallbackInfo,
                                   pStartNextOperation );

    if( status != AWS_IOT_JOBS_SUCCESS )
    {
        IOT_GOTO_CLEANUP();
    }

    /* Check update info. */
    status = _validateUpdateInfo( JOBS_START_NEXT,
                                  pUpdateInfo );

    if( status != AWS_IOT_JOBS_SUCCESS )
    {
        IOT_GOTO_CLEANUP();
    }

    /* Allocate a new Jobs operation. */
    requestContents.pUpdateInfo = pUpdateInfo;

    status = _AwsIotJobs_CreateOperation( JOBS_START_NEXT,
                                          pRequestInfo,
                                          &requestContents,
                                          flags,
                                          pCallbackInfo,
                                          &pOperation );

    if( status != AWS_IOT_JOBS_SUCCESS )
    {
        /* No memory for Jobs operation. */
        IOT_GOTO_CLEANUP();
    }

    /* Set the reference if provided. This must be done before the Jobs operation
     * is processed. */
    if( pStartNextOperation != NULL )
    {
        *pStartNextOperation = pOperation;
    }

    /* Process the Jobs operation. This subscribes to any required topics and
     * sends the MQTT message for the Jobs operation. */
    status = _AwsIotJobs_ProcessOperation( pRequestInfo, pOperation );

    /* If the Jobs operation failed, clear the now invalid reference. */
    if( ( status != AWS_IOT_JOBS_STATUS_PENDING ) && ( pStartNextOperation != NULL ) )
    {
        *pStartNextOperation = AWS_IOT_JOBS_OPERATION_INITIALIZER;
    }

    IOT_FUNCTION_EXIT_NO_CLEANUP();
}

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

AwsIotJobsError_t AwsIotJobs_StartNextSync( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                            const AwsIotJobsUpdateInfo_t * pUpdateInfo,
                                            uint32_t flags,
                                            uint32_t timeoutMs,
                                            AwsIotJobsResponse_t * const pJobsResponse )
{
    AwsIotJobsError_t status = AWS_IOT_JOBS_STATUS_PENDING;
    AwsIotJobsOperation_t startNextOperation = AWS_IOT_JOBS_OPERATION_INITIALIZER;

    /* Set the waitable flag. */
    flags |= AWS_IOT_JOBS_FLAG_WAITABLE;

    /* Call the asynchronous Jobs Start Next function. */
    status = AwsIotJobs_StartNextAsync( pRequestInfo,
                                        pUpdateInfo,
                                        flags,
                                        NULL,
                                        &startNextOperation );

    /* Wait for the Jobs Start Next operation to complete. */
    if( status == AWS_IOT_JOBS_STATUS_PENDING )
    {
        status = AwsIotJobs_Wait( startNextOperation,
                                  timeoutMs,
                                  pJobsResponse );
    }

    return status;
}

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

AwsIotJobsError_t AwsIotJobs_DescribeAsync( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                            int32_t executionNumber,
                                            bool includeJobDocument,
                                            uint32_t flags,
                                            const AwsIotJobsCallbackInfo_t * pCallbackInfo,
                                            AwsIotJobsOperation_t * const pDescribeOperation )
{
    IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_STATUS_PENDING );
    _jobsOperation_t * pOperation = NULL;
    _jsonRequestContents_t requestContents = { 0 };

    /* Check that AwsIotJobs_Init was called. */
    if( _checkInit() == false )
    {
        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NOT_INITIALIZED );
    }

    /* Check request info. */
    status = _validateRequestInfo( JOBS_DESCRIBE,
                                   pRequestInfo,
                                   flags,
                                   pCallbackInfo,
                                   pDescribeOperation );

    if( status != AWS_IOT_JOBS_SUCCESS )
    {
        IOT_GOTO_CLEANUP();
    }

    /* Allocate a new Jobs operation. */
    requestContents.describe.executionNumber = executionNumber;
    requestContents.describe.includeJobDocument = includeJobDocument;

    status = _AwsIotJobs_CreateOperation( JOBS_DESCRIBE,
                                          pRequestInfo,
                                          &requestContents,
                                          flags,
                                          pCallbackInfo,
                                          &pOperation );

    if( status != AWS_IOT_JOBS_SUCCESS )
    {
        /* No memory for Jobs operation. */
        IOT_GOTO_CLEANUP();
    }

    /* Set the reference if provided. This must be done before the Jobs operation
     * is processed. */
    if( pDescribeOperation != NULL )
    {
        *pDescribeOperation = pOperation;
    }

    /* Process the Jobs operation. This subscribes to any required topics and
     * sends the MQTT message for the Jobs operation. */
    status = _AwsIotJobs_ProcessOperation( pRequestInfo, pOperation );

    /* If the Jobs operation failed, clear the now invalid reference. */
    if( ( status != AWS_IOT_JOBS_STATUS_PENDING ) && ( pDescribeOperation != NULL ) )
    {
        *pDescribeOperation = AWS_IOT_JOBS_OPERATION_INITIALIZER;
    }

    IOT_FUNCTION_EXIT_NO_CLEANUP();
}

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

AwsIotJobsError_t AwsIotJobs_DescribeSync( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                           int32_t executionNumber,
                                           bool includeJobDocument,
                                           uint32_t flags,
                                           uint32_t timeoutMs,
                                           AwsIotJobsResponse_t * const pJobsResponse )
{
    AwsIotJobsError_t status = AWS_IOT_JOBS_STATUS_PENDING;
    AwsIotJobsOperation_t describeOperation = AWS_IOT_JOBS_OPERATION_INITIALIZER;

    /* Set the waitable flag. */
    flags |= AWS_IOT_JOBS_FLAG_WAITABLE;

    /* Call the asynchronous Jobs Describe function. */
    status = AwsIotJobs_DescribeAsync( pRequestInfo,
                                       executionNumber,
                                       includeJobDocument,
                                       flags,
                                       NULL,
                                       &describeOperation );

    /* Wait for the Jobs Describe operation to complete. */
    if( status == AWS_IOT_JOBS_STATUS_PENDING )
    {
        status = AwsIotJobs_Wait( describeOperation,
                                  timeoutMs,
                                  pJobsResponse );
    }

    return status;
}

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

AwsIotJobsError_t AwsIotJobs_UpdateAsync( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                          const AwsIotJobsUpdateInfo_t * pUpdateInfo,
                                          uint32_t flags,
                                          const AwsIotJobsCallbackInfo_t * pCallbackInfo,
                                          AwsIotJobsOperation_t * const pUpdateOperation )
{
    IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_STATUS_PENDING );
    _jobsOperation_t * pOperation = NULL;
    _jsonRequestContents_t requestContents = { 0 };

    /* Check that AwsIotJobs_Init was called. */
    if( _checkInit() == false )
    {
        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NOT_INITIALIZED );
    }

    /* Check request info. */
    status = _validateRequestInfo( JOBS_UPDATE,
                                   pRequestInfo,
                                   flags,
                                   pCallbackInfo,
                                   pUpdateOperation );

    if( status != AWS_IOT_JOBS_SUCCESS )
    {
        IOT_GOTO_CLEANUP();
    }

    /* Check update info. */
    status = _validateUpdateInfo( JOBS_UPDATE,
                                  pUpdateInfo );

    if( status != AWS_IOT_JOBS_SUCCESS )
    {
        IOT_GOTO_CLEANUP();
    }

    /* Allocate a new Jobs operation. */
    requestContents.pUpdateInfo = pUpdateInfo;

    status = _AwsIotJobs_CreateOperation( JOBS_UPDATE,
                                          pRequestInfo,
                                          &requestContents,
                                          flags,
                                          pCallbackInfo,
                                          &pOperation );

    if( status != AWS_IOT_JOBS_SUCCESS )
    {
        /* No memory for Jobs operation. */
        IOT_GOTO_CLEANUP();
    }

    /* Set the reference if provided. This must be done before the Jobs operation
     * is processed. */
    if( pUpdateOperation != NULL )
    {
        *pUpdateOperation = pOperation;
    }

    /* Process the Jobs operation. This subscribes to any required topics and
     * sends the MQTT message for the Jobs operation. */
    status = _AwsIotJobs_ProcessOperation( pRequestInfo, pOperation );

    /* If the Jobs operation failed, clear the now invalid reference. */
    if( ( status != AWS_IOT_JOBS_STATUS_PENDING ) && ( pUpdateOperation != NULL ) )
    {
        *pUpdateOperation = AWS_IOT_JOBS_OPERATION_INITIALIZER;
    }

    IOT_FUNCTION_EXIT_NO_CLEANUP();
}

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

AwsIotJobsError_t AwsIotJobs_UpdateSync( const AwsIotJobsRequestInfo_t * pRequestInfo,
                                         const AwsIotJobsUpdateInfo_t * pUpdateInfo,
                                         uint32_t flags,
                                         uint32_t timeoutMs,
                                         AwsIotJobsResponse_t * const pJobsResponse )
{
    AwsIotJobsError_t status = AWS_IOT_JOBS_STATUS_PENDING;
    AwsIotJobsOperation_t updateOperation = AWS_IOT_JOBS_OPERATION_INITIALIZER;

    /* Set the waitable flag. */
    flags |= AWS_IOT_JOBS_FLAG_WAITABLE;

    /* Call the asynchronous Jobs Update function. */
    status = AwsIotJobs_UpdateAsync( pRequestInfo,
                                     pUpdateInfo,
                                     flags,
                                     NULL,
                                     &updateOperation );

    /* Wait for the Jobs Update operation to complete. */
    if( status == AWS_IOT_JOBS_STATUS_PENDING )
    {
        status = AwsIotJobs_Wait( updateOperation,
                                  timeoutMs,
                                  pJobsResponse );
    }

    return status;
}

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

AwsIotJobsError_t AwsIotJobs_Wait( AwsIotJobsOperation_t operation,
                                   uint32_t timeoutMs,
                                   AwsIotJobsResponse_t * const pJobsResponse )
{
    IOT_FUNCTION_ENTRY( AwsIotJobsError_t, AWS_IOT_JOBS_STATUS_PENDING );

    /* Check that AwsIotJobs_Init was called. */
    if( _checkInit() == false )
    {
        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_NOT_INITIALIZED );
    }

    /* Check that reference is set. */
    if( operation == NULL )
    {
        IotLogError( "Operation reference cannot be NULL." );

        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
    }

    /* Check that reference is waitable. */
    if( ( operation->flags & AWS_IOT_JOBS_FLAG_WAITABLE ) == 0 )
    {
        IotLogError( "Operation is not waitable." );

        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
    }

    /* Check that output parameter is set. */
    if( pJobsResponse == NULL )
    {
        IotLogError( "Output Jobs response parameter must be set for Jobs %s.",
                     _pAwsIotJobsOperationNames[ operation->type ] );

        IOT_SET_AND_GOTO_CLEANUP( AWS_IOT_JOBS_BAD_PARAMETER );
    }

    /* Wait for a response to the Jobs operation. */
    if( IotSemaphore_TimedWait( &( operation->notify.waitSemaphore ),
                                timeoutMs ) == true )
    {
        status = operation->status;
    }
    else
    {
        status = AWS_IOT_JOBS_TIMEOUT;
    }

    /* Remove the completed operation from the pending operation list. */
    IotMutex_Lock( &( _AwsIotJobsPendingOperationsMutex ) );
    IotListDouble_Remove( &( operation->link ) );
    IotMutex_Unlock( &( _AwsIotJobsPendingOperationsMutex ) );

    /* Decrement the reference count. This also removes subscriptions if the
     * count reaches 0. */
    IotMutex_Lock( &_AwsIotJobsSubscriptionsMutex );
    _AwsIotJobs_DecrementReferences( operation,
                                     operation->pSubscription->pTopicBuffer,
                                     NULL );
    IotMutex_Unlock( &_AwsIotJobsSubscriptionsMutex );

    /* Set the output parameters. Jobs responses are available on success or
     * when the Jobs service returns an error document. */
    if( ( status == AWS_IOT_JOBS_SUCCESS ) || ( status > AWS_IOT_JOBS_INVALID_TOPIC ) )
    {
        AwsIotJobs_Assert( operation->pJobsResponse != NULL );
        AwsIotJobs_Assert( operation->jobsResponseLength > 0 );

        pJobsResponse->pJobsResponse = operation->pJobsResponse;
        pJobsResponse->jobsResponseLength = operation->jobsResponseLength;
    }

    /* Destroy the Jobs operation. */
    _AwsIotJobs_DestroyOperation( operation );

    IOT_FUNCTION_EXIT_NO_CLEANUP();
}

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

AwsIotJobsError_t AwsIotJobs_SetNotifyPendingCallback( IotMqttConnection_t mqttConnection,
                                                       const char * pThingName,
                                                       size_t thingNameLength,
                                                       uint32_t flags,
                                                       const AwsIotJobsCallbackInfo_t * pNotifyPendingCallback )
{
    /* Flags are not currently used by this function. */
    ( void ) flags;

    return _setCallbackCommon( mqttConnection,
                               NOTIFY_PENDING_CALLBACK,
                               pThingName,
                               thingNameLength,
                               pNotifyPendingCallback );
}

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

AwsIotJobsError_t AwsIotJobs_SetNotifyNextCallback( IotMqttConnection_t mqttConnection,
                                                    const char * pThingName,
                                                    size_t thingNameLength,
                                                    uint32_t flags,
                                                    const AwsIotJobsCallbackInfo_t * pNotifyNextCallback )
{
    /* Flags are not currently used by this function. */
    ( void ) flags;

    return _setCallbackCommon( mqttConnection,
                               NOTIFY_NEXT_CALLBACK,
                               pThingName,
                               thingNameLength,
                               pNotifyNextCallback );
}

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

const char * AwsIotJobs_strerror( AwsIotJobsError_t status )
{
    const char * pMessage = NULL;

    switch( status )
    {
        case AWS_IOT_JOBS_SUCCESS:
            pMessage = "SUCCESS";
            break;

        case AWS_IOT_JOBS_STATUS_PENDING:
            pMessage = "STATUS PENDING";
            break;

        case AWS_IOT_JOBS_INIT_FAILED:
            pMessage = "INIT FAILED";
            break;

        case AWS_IOT_JOBS_BAD_PARAMETER:
            pMessage = "BAD PARAMETER";
            break;

        case AWS_IOT_JOBS_NO_MEMORY:
            pMessage = "NO MEMORY";
            break;

        case AWS_IOT_JOBS_MQTT_ERROR:
            pMessage = "MQTT ERROR";
            break;

        case AWS_IOT_JOBS_BAD_RESPONSE:
            pMessage = "BAD RESPONSE";
            break;

        case AWS_IOT_JOBS_TIMEOUT:
            pMessage = "TIMEOUT";
            break;

        case AWS_IOT_JOBS_NOT_INITIALIZED:
            pMessage = "NOT INITIALIZED";
            break;

        case AWS_IOT_JOBS_INVALID_TOPIC:
            pMessage = "FAILED: INVALID TOPIC";
            break;

        case AWS_IOT_JOBS_INVALID_JSON:
            pMessage = "FAILED: INVALID JSON";
            break;

        case AWS_IOT_JOBS_INVALID_REQUEST:
            pMessage = "FAILED: INVALID REQUEST";
            break;

        case AWS_IOT_JOBS_INVALID_STATE:
            pMessage = "FAILED: INVALID STATE TRANSITION";
            break;

        case AWS_IOT_JOBS_NOT_FOUND:
            pMessage = "FAILED: NOT FOUND";
            break;

        case AWS_IOT_JOBS_VERSION_MISMATCH:
            pMessage = "FAILED: VERSION MISMATCH";
            break;

        case AWS_IOT_JOBS_INTERNAL_ERROR:
            pMessage = "FAILED: INTERNAL ERROR";
            break;

        case AWS_IOT_JOBS_THROTTLED:
            pMessage = "FAILED: THROTTLED";
            break;

        case AWS_IOT_JOBS_TERMINAL_STATE:
            pMessage = "FAILED: TERMINAL STATE";
            break;

        default:
            pMessage = "INVALID STATUS";
            break;
    }

    return pMessage;
}

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

const char * AwsIotJobs_StateName( AwsIotJobState_t state )
{
    const char * pMessage = NULL;

    switch( state )
    {
        case AWS_IOT_JOB_STATE_QUEUED:
            pMessage = "QUEUED";
            break;

        case AWS_IOT_JOB_STATE_IN_PROGRESS:
            pMessage = "IN PROGRESS";
            break;

        case AWS_IOT_JOB_STATE_FAILED:
            pMessage = "FAILED";
            break;

        case AWS_IOT_JOB_STATE_SUCCEEDED:
            pMessage = "SUCCEEDED";
            break;

        case AWS_IOT_JOB_STATE_CANCELED:
            pMessage = "CANCELED";
            break;

        case AWS_IOT_JOB_STATE_TIMED_OUT:
            pMessage = "TIMED OUT";
            break;

        case AWS_IOT_JOB_STATE_REJECTED:
            pMessage = "REJECTED";
            break;

        case AWS_IOT_JOB_STATE_REMOVED:
            pMessage = "REMOVED";
            break;

        default:
            pMessage = "INVALID STATE";
            break;
    }

    return pMessage;
}

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