// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#include <stdlib.h>

#include "azure_c_shared_utility/gballoc.h"
#include "azure_c_shared_utility/xlogging.h"
#include "azure_c_shared_utility/crt_abstractions.h"
#include "azure_c_shared_utility/string_tokenizer.h"
#include "azure_c_shared_utility/strings.h"
#include "azure_c_shared_utility/platform.h"
#include "azure_c_shared_utility/sastoken.h"
#include "azure_c_shared_utility/urlencode.h"
#include "azure_c_shared_utility/connection_string_parser.h"
#include "azure_c_shared_utility/tlsio.h"
#include "azure_c_shared_utility/http_proxy_io.h"

#include "azure_uhttp_c/uhttp.h"

#include "prov_service_client/provisioning_service_client.h"
#include "prov_service_client/provisioning_sc_models_serializer.h"
#include "prov_service_client/provisioning_sc_shared_helpers.h"

typedef enum HTTP_CONNECTION_STATE_TAG
{
    HTTP_STATE_DISCONNECTED,
    HTTP_STATE_CONNECTING,
    HTTP_STATE_CONNECTED,
    HTTP_STATE_REQUEST_SENT,
    HTTP_STATE_REQUEST_RECV,
    HTTP_STATE_COMPLETE,
    HTTP_STATE_ERROR
} HTTP_CONNECTION_STATE;

//consider substructure representing SharedAccessSignature?
typedef struct PROVISIONING_SERVICE_CLIENT_TAG
{
    //Connection details
    char* provisioning_service_uri;
    char* key_name;
    char* access_key;

    //Connection data
    HTTP_CONNECTION_STATE http_state;
    char* response;
    HTTP_HEADERS_HANDLE response_headers;

    //Connection options
    TRACING_STATUS tracing;
    HTTP_PROXY_OPTIONS* proxy_options;
    char* certificate;

} PROV_SERVICE_CLIENT;

typedef char*(*VECTOR_SERIALIZE_TO_JSON)(void*);
typedef void*(*VECTOR_DESERIALIZE_FROM_JSON)(char*);
typedef char*(*VECTOR_GET_ID)(void*);
typedef char*(*VECTOR_GET_ETAG)(void*);
typedef void(*VECTOR_DESTROY)(void*);

typedef struct HANDLE_FUNCTION_VECTOR_TAG
{
    VECTOR_SERIALIZE_TO_JSON serializeToJson;
    VECTOR_DESERIALIZE_FROM_JSON deserializeFromJson;
    VECTOR_GET_ID getId;
    VECTOR_GET_ETAG getEtag;
    VECTOR_DESTROY destroy;
} HANDLE_FUNCTION_VECTOR;

static const char* const IOTHUBHOSTNAME =                       "HostName";
static const char* const IOTHUBSHAREDACESSKEYNAME =             "SharedAccessKeyName";
static const char* const IOTHUBSHAREDACESSKEY =                 "SharedAccessKey";
static const char* const PROVISIONING_SERVICE_API_VERSION =     "2018-04-01";
static const char* const ENROLL_GROUP_PROVISION_PATH_FMT =      "/enrollmentGroups/%s";
static const char* const INDV_ENROLL_PROVISION_PATH_FMT =       "/enrollments/%s";
static const char* const REG_STATE_PROVISION_PATH_FMT =         "/registrations/%s";
static const char* const REG_STATE_QUERY_PATH_FMT =             "/registrations/%s/query";
static const char* const INDV_ENROLL_BULK_PATH_FMT =            "/enrollments/";
static const char* const INDV_ENROLL_QUERY_PATH_FMT =           "/enrollments/query";
static const char* const ENROLL_GROUP_QUERY_PATH_FMT =          "/enrollmentGroups/query";
static const char* const API_VERSION_QUERY_PARAM =              "?api-version=%s";
static const char* const HEADER_KEY_AUTHORIZATION =             "Authorization";
static const char* const HEADER_KEY_IF_MATCH =                  "If-Match";
static const char* const HEADER_KEY_USER_AGENT =                "UserAgent";
static const char* const HEADER_KEY_ACCEPT =                    "Accept";
static const char* const HEADER_KEY_CONTENT_TYPE =              "Content-Type";
static const char* const HEADER_KEY_CONTINUATION =              "x-ms-continuation";
static const char* const HEADER_KEY_MAX_ITEM_COUNT =            "x-ms-max-item-count";
static const char* const HEADER_KEY_ITEM_TYPE =                 "x-ms-item-type";
static const char* const HEADER_VALUE_USER_AGENT =              "iothub_dps_prov_client/1.0";
static const char* const HEADER_VALUE_ACCEPT =                  "application/json";
static const char* const HEADER_VALUE_CONTENT_TYPE =            "application/json; charset=utf-8";

#define DEFAULT_HTTPS_PORT          443
#define UID_LENGTH                  37
#define SAS_TOKEN_DEFAULT_LIFETIME  3600
#define EPOCH_TIME_T_VALUE          (time_t)0

static HANDLE_FUNCTION_VECTOR getVector_individualEnrollment(void)
{
    HANDLE_FUNCTION_VECTOR vector;
    vector.serializeToJson = (VECTOR_SERIALIZE_TO_JSON)individualEnrollment_serializeToJson;
    vector.deserializeFromJson = (VECTOR_DESERIALIZE_FROM_JSON)individualEnrollment_deserializeFromJson;
    vector.getId = (VECTOR_GET_ID)individualEnrollment_getRegistrationId;
    vector.getEtag = (VECTOR_GET_ETAG)individualEnrollment_getEtag;
    vector.destroy = (VECTOR_DESTROY)individualEnrollment_destroy;

    return vector;
}

static HANDLE_FUNCTION_VECTOR getVector_enrollmentGroup(void)
{
    HANDLE_FUNCTION_VECTOR vector;
    vector.serializeToJson = (VECTOR_SERIALIZE_TO_JSON)enrollmentGroup_serializeToJson;
    vector.deserializeFromJson = (VECTOR_DESERIALIZE_FROM_JSON)enrollmentGroup_deserializeFromJson;
    vector.getId = (VECTOR_GET_ID)enrollmentGroup_getGroupId;
    vector.getEtag = (VECTOR_GET_ETAG)enrollmentGroup_getEtag;
    vector.destroy = (VECTOR_DESTROY)enrollmentGroup_destroy;

    return vector;
}

static HANDLE_FUNCTION_VECTOR getVector_registrationState(void)
{
    HANDLE_FUNCTION_VECTOR vector;
    vector.serializeToJson = NULL;
    vector.deserializeFromJson = (VECTOR_DESERIALIZE_FROM_JSON)deviceRegistrationState_deserializeFromJson;
    vector.getId = (VECTOR_GET_ID)deviceRegistrationState_getRegistrationId;
    vector.getEtag = (VECTOR_GET_ETAG)deviceRegistrationState_getEtag;
    vector.destroy = (VECTOR_DESTROY)deviceRegistrationState_destroy;

    return vector;
}

static void on_http_connected(void* callback_ctx, HTTP_CALLBACK_REASON connect_result)
{
    if (callback_ctx != NULL)
    {
        PROV_SERVICE_CLIENT* prov_client = (PROV_SERVICE_CLIENT*)callback_ctx;
        if (connect_result == HTTP_CALLBACK_REASON_OK)
        {
            prov_client->http_state = HTTP_STATE_CONNECTED;
        }
        else
        {
            prov_client->http_state = HTTP_STATE_ERROR;
        }
    }
}

static void on_http_error(void* callback_ctx, HTTP_CALLBACK_REASON error_result)
{
    (void)error_result;
    if (callback_ctx != NULL)
    {
        PROV_SERVICE_CLIENT* prov_client = (PROV_SERVICE_CLIENT*)callback_ctx;
        prov_client->http_state = HTTP_STATE_ERROR;
        LogError("Failure encountered in http %d", error_result);
    }
    else
    {
        LogError("Failure encountered in http %d", error_result);
    }
}

static void on_http_reply_recv(void* callback_ctx, HTTP_CALLBACK_REASON request_result, const unsigned char* content, size_t content_len, unsigned int status_code, HTTP_HEADERS_HANDLE responseHeadersHandle)
{
    (void)content_len;
    if (callback_ctx != NULL)
    {
        PROV_SERVICE_CLIENT* prov_client = (PROV_SERVICE_CLIENT*)callback_ctx;
        const char* content_str = (const char*)content;

        //attach headers to prov_client
        if (responseHeadersHandle != NULL)
        {
            if ((prov_client->response_headers = HTTPHeaders_Clone(responseHeadersHandle)) == NULL)
            {
                LogError("Copying response headers failed");
                prov_client->response_headers = NULL;
            }
        }

        //if there is a json response
        if (content != NULL)
        {
            if ((prov_client->response = malloc(content_len + 1)) == NULL)
            {
                LogError("Allocating response failed");
                prov_client->response = NULL;
            }
            else
            {
                memset(prov_client->response, 0, content_len);
                memcpy(prov_client->response, content_str, content_len);
            }
        }

        //update HTTP state
        if (request_result == HTTP_CALLBACK_REASON_OK)
        {
            if (status_code >= 200 && status_code <= 299)
            {
                prov_client->http_state = HTTP_STATE_REQUEST_RECV;
            }
            else
            {
                prov_client->http_state = HTTP_STATE_ERROR;
            }
        }
        else
        {
            prov_client->http_state = HTTP_STATE_ERROR;
        }
    }
    else
    {
        LogError("Invalid callback context");
    }
}

static HTTP_HEADERS_HANDLE construct_http_headers(const PROV_SERVICE_CLIENT* prov_client, const char* etag, HTTP_CLIENT_REQUEST_TYPE request)
{
    HTTP_HEADERS_HANDLE result;
    if ((result = HTTPHeaders_Alloc()) == NULL)
    {
        LogError("failure sending request");
    }
    else
    {
        size_t secSinceEpoch = (size_t)(difftime(get_time(NULL), EPOCH_TIME_T_VALUE) + 0);
        size_t expiryTime = secSinceEpoch + SAS_TOKEN_DEFAULT_LIFETIME;

        STRING_HANDLE sas_token = SASToken_CreateString(prov_client->access_key, prov_client->provisioning_service_uri, prov_client->key_name, expiryTime);
        if (sas_token == NULL)
        {
            HTTPHeaders_Free(result);
            result = NULL;
        }
        else
        {
            if ((HTTPHeaders_AddHeaderNameValuePair(result, HEADER_KEY_USER_AGENT, HEADER_VALUE_USER_AGENT) != HTTP_HEADERS_OK) ||
                (HTTPHeaders_AddHeaderNameValuePair(result, HEADER_KEY_ACCEPT, HEADER_VALUE_ACCEPT) != HTTP_HEADERS_OK) ||
                ((request != HTTP_CLIENT_REQUEST_DELETE) && (HTTPHeaders_AddHeaderNameValuePair(result, HEADER_KEY_CONTENT_TYPE, HEADER_VALUE_CONTENT_TYPE) != HTTP_HEADERS_OK)) ||
                (HTTPHeaders_AddHeaderNameValuePair(result, HEADER_KEY_AUTHORIZATION, STRING_c_str(sas_token)) != HTTP_HEADERS_OK) ||
                ((etag != NULL) && (HTTPHeaders_AddHeaderNameValuePair(result, HEADER_KEY_IF_MATCH, etag) != HTTP_HEADERS_OK)))
            {
                LogError("failure adding header value");
                HTTPHeaders_Free(result);
                result = NULL;
            }
            STRING_delete(sas_token);
        }
    }
    return result;
}

static int add_query_headers(HTTP_HEADERS_HANDLE headers, size_t page_size, const char* cont_token)
{
    int result = 0;

    if (cont_token != NULL)
    {
        if (HTTPHeaders_AddHeaderNameValuePair(headers, HEADER_KEY_CONTINUATION, cont_token) != HTTP_HEADERS_OK)
        {
            LogError("Failure adding continuation token header");
            result = MU_FAILURE;
        }
    }

    if (result == 0)
    {
        if (page_size != NO_MAX_PAGE_SIZE)
        {
            char page_size_s[21]; //21 characters covers all 64bit numbers
            sprintf(page_size_s, "%d", (int)page_size);
            if (HTTPHeaders_AddHeaderNameValuePair(headers, HEADER_KEY_MAX_ITEM_COUNT, page_size_s) != HTTP_HEADERS_OK)
            {
                LogError("Failure adding max item count header");
                result = MU_FAILURE;
            }
        }
    }

    return result;
}

static STRING_HANDLE create_registration_path(const char* path_format, const char* id)
{
    STRING_HANDLE registration_path;

    if (id == NULL)
    {
        registration_path = STRING_construct(path_format);
    }
    else
    {
        STRING_HANDLE encoded_id;
        if ((encoded_id = URL_EncodeString(id)) == NULL)
        {
            LogError("Unable to URL encode ID");
            registration_path = NULL;
        }
        else
        {
            registration_path = STRING_construct_sprintf(path_format, STRING_c_str(encoded_id));
            STRING_delete(encoded_id);
        }
    }

    if (registration_path == NULL)
    {
        LogError("Failed constructing base path");
    }
    else if (STRING_sprintf(registration_path, API_VERSION_QUERY_PARAM, PROVISIONING_SERVICE_API_VERSION) != 0)
    {
        LogError("Unable to add query paramters");
        STRING_delete(registration_path);
        registration_path = NULL;
    }

    return registration_path;
}

static int get_response_headers(PROV_SERVICE_CLIENT* prov_client, char** cont_token_ptr, const char** resp_type_ptr)
{
    int result = 0;
    HTTP_HEADERS_HANDLE resp_headers = prov_client->response_headers;
    if (resp_headers == NULL)
    {
        LogError("Unable to retrieve headers");
        result = MU_FAILURE;
    }
    else
    {
        if (cont_token_ptr != NULL)
        {
            const char* cont_token = HTTPHeaders_FindHeaderValue(resp_headers, HEADER_KEY_CONTINUATION);
            if (cont_token != NULL)
            {
                if (mallocAndStrcpy_s(cont_token_ptr, cont_token) != 0)
                {
                    LogError("Failed to copy continuation token");
                    result = MU_FAILURE;
                }
            }
            else
            {
                *cont_token_ptr = NULL;
            }
        }

        if (resp_type_ptr != NULL)
        {
            *resp_type_ptr = HTTPHeaders_FindHeaderValue(resp_headers, HEADER_KEY_ITEM_TYPE);
        }
    }

    return result;
}

static HTTP_CLIENT_HANDLE connect_to_service(PROV_SERVICE_CLIENT* prov_client)
{
    HTTP_CLIENT_HANDLE result;

    // Create uhttp
    TLSIO_CONFIG tls_io_config;
    HTTP_PROXY_IO_CONFIG http_proxy;
    memset(&tls_io_config, 0, sizeof(TLSIO_CONFIG));
    tls_io_config.hostname = prov_client->provisioning_service_uri;
    tls_io_config.port = DEFAULT_HTTPS_PORT;

     // Setup proxy
     if (prov_client->proxy_options != NULL)
     {
         memset(&http_proxy, 0, sizeof(HTTP_PROXY_IO_CONFIG));
         http_proxy.hostname = prov_client->provisioning_service_uri;
         http_proxy.port = DEFAULT_HTTPS_PORT;
         http_proxy.proxy_hostname = prov_client->proxy_options->host_address;
         http_proxy.proxy_port = prov_client->proxy_options->port;
         http_proxy.username = prov_client->proxy_options->username;
         http_proxy.password = prov_client->proxy_options->password;

         tls_io_config.underlying_io_interface = http_proxy_io_get_interface_description();
         tls_io_config.underlying_io_parameters = &http_proxy;
     }

    const IO_INTERFACE_DESCRIPTION* interface_desc = platform_get_default_tlsio();
    if (interface_desc == NULL)
    {
        LogError("platform default tlsio is NULL");
        result = NULL;
    }
    else if ((result = uhttp_client_create(interface_desc, &tls_io_config, on_http_error, prov_client)) == NULL)
    {
        LogError("Failed creating http object");
    }
    else if (prov_client->certificate != NULL && uhttp_client_set_trusted_cert(result, prov_client->certificate) != HTTP_CLIENT_OK)
    {
         LogError("Failed setting trusted cert");
         uhttp_client_destroy(result);
         result = NULL;
     }
    else if (prov_client->tracing == TRACING_STATUS_ON && uhttp_client_set_trace(result, true, true) != HTTP_CLIENT_OK)
    {
        LogError("Failed setting trace");
        uhttp_client_destroy(result);
        result = NULL;
    }
    else if (uhttp_client_open(result, prov_client->provisioning_service_uri, DEFAULT_HTTPS_PORT, on_http_connected, prov_client) != HTTP_CLIENT_OK)
    {
        LogError("Failed opening http url %s", prov_client->provisioning_service_uri);
        uhttp_client_destroy(result);
        result = NULL;
    }
    return result;
}

static int rest_call(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, HTTP_CLIENT_REQUEST_TYPE operation, const char* registration_path, HTTP_HEADERS_HANDLE request_headers, const char* content)
{
    int result;
    size_t content_len;
    HTTP_CLIENT_HANDLE http_client;

    if (content == NULL)
    {
        content_len = 0;
    }
    else
    {
        content_len = strlen(content);
    }

    http_client = connect_to_service(prov_client);
    if (http_client == NULL)
    {
        LogError("Failed connecting to service");
        result = MU_FAILURE;
    }
    else
    {
        result = 0;
        do
        {
            uhttp_client_dowork(http_client);
            if (prov_client->http_state == HTTP_STATE_CONNECTED)
            {
                if (uhttp_client_execute_request(http_client, operation, registration_path, request_headers, (unsigned char*)content, content_len, on_http_reply_recv, prov_client) != HTTP_CLIENT_OK)
                {
                    LogError("Failure executing http request");
                    prov_client->http_state = HTTP_STATE_ERROR;
                    result = MU_FAILURE;
                }
                else
                {
                    prov_client->http_state = HTTP_STATE_REQUEST_SENT;
                }
            }
            else if (prov_client->http_state == HTTP_STATE_REQUEST_RECV)
            {
                prov_client->http_state = HTTP_STATE_COMPLETE;
            }
            else if (prov_client->http_state == HTTP_STATE_ERROR)
            {
                result = MU_FAILURE;
                LogError("HTTP error");
            }
        } while (prov_client->http_state != HTTP_STATE_COMPLETE && prov_client->http_state != HTTP_STATE_ERROR);

        uhttp_client_close(http_client, NULL, NULL);
        uhttp_client_destroy(http_client);
    }

    prov_client->http_state = HTTP_STATE_DISCONNECTED;
    return result;
}

static void clear_response(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client)
{
    free(prov_client->response);
    prov_client->response = NULL;
    HTTPHeaders_Free(prov_client->response_headers);
    prov_client->response_headers = NULL;
}

static int prov_sc_create_or_update_record(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, void** handle_ptr, HANDLE_FUNCTION_VECTOR vector, const char* path_format)
{
    int result = 0;
    void* handle;

    if (prov_client == NULL)
    {
        LogError("Invalid Provisioning Client Handle");
        result = MU_FAILURE;
    }
    else if ((handle_ptr == NULL) || ((handle = *handle_ptr) == NULL))
    {
        LogError("Invalid handle");
        result = MU_FAILURE;
    }
    else
    {
        char* content;
        if ((content = vector.serializeToJson(handle)) == NULL)
        {
            LogError("Failure serializing enrollment");
            result = MU_FAILURE;
        }
        else
        {
            STRING_HANDLE registration_path = NULL;
            const char* id = NULL;
            if ((id = vector.getId(handle)) == NULL)
            {
                LogError("Given model does not have a valid ID");
                result = MU_FAILURE;
            }
            else if ((registration_path = create_registration_path(path_format, id)) == NULL)
            {
                LogError("Failed to construct a registration path");
                result = MU_FAILURE;
            }
            else
            {
                HTTP_HEADERS_HANDLE request_headers;
                if ((request_headers = construct_http_headers(prov_client, vector.getEtag(handle), HTTP_CLIENT_REQUEST_PUT)) == NULL)
                {
                    LogError("Failure constructing headers");
                    result = MU_FAILURE;
                }
                else
                {
                    result = rest_call(prov_client, HTTP_CLIENT_REQUEST_PUT, STRING_c_str(registration_path), request_headers, content);

                    if (result == 0)
                    {
                        INDIVIDUAL_ENROLLMENT_HANDLE new_handle;
                        if ((new_handle = vector.deserializeFromJson(prov_client->response)) == NULL)
                        {
                            LogError("Failure constructing new enrollment structure from json response");
                            result = MU_FAILURE;
                        }

                        //Free the user submitted enrollment, and replace the pointer reference to a new enrollment from the provisioning service
                        vector.destroy(handle);
                        *handle_ptr = new_handle;
                    }
                    else
                    {
                        LogError("Rest call failed");
                    }
                    clear_response(prov_client);
                }
                HTTPHeaders_Free(request_headers);
            }
            STRING_delete(registration_path);
        }
        free(content);
    }

    return result;
}

static int prov_sc_delete_record_by_param(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, const char* id, const char* etag, const char* path_format)
{
    int result = 0;

    if (prov_client == NULL)
    {
        LogError("Invalid Provisioning Client Handle");
        result = MU_FAILURE;
    }
    else if (id == NULL)
    {
        LogError("Invalid Id");
        result = MU_FAILURE;
    }
    else
    {
        STRING_HANDLE registration_path = create_registration_path(path_format, id);
        if (registration_path == NULL)
        {
            LogError("Failed to construct a registration path");
            result = MU_FAILURE;
        }
        else
        {
            HTTP_HEADERS_HANDLE request_headers;
            if ((request_headers = construct_http_headers(prov_client, etag, HTTP_CLIENT_REQUEST_DELETE)) == NULL)
            {
                LogError("Failure constructing http headers");
                result = MU_FAILURE;
            }
            else
            {
                result = rest_call(prov_client, HTTP_CLIENT_REQUEST_DELETE, STRING_c_str(registration_path), request_headers, NULL);
                clear_response(prov_client);
            }
            HTTPHeaders_Free(request_headers);
        }
        STRING_delete(registration_path);
    }

    return result;
}

static int prov_sc_get_record(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, const char* id, void** handle_ptr, HANDLE_FUNCTION_VECTOR vector, const char* path_format)
{
    int result = 0;

    if (prov_client == NULL)
    {
        LogError("Invalid Provisioning Client Handle");
        result = MU_FAILURE;
    }
    else if (id == NULL)
    {
        LogError("Invalid id");
        result = MU_FAILURE;
    }
    else if (handle_ptr == NULL)
    {
        LogError("Invalid handle");
        result = MU_FAILURE;
    }
    else
    {
        STRING_HANDLE registration_path = create_registration_path(path_format, id);
        if (registration_path == NULL)
        {
            LogError("Failed to construct a registration path");
            result = MU_FAILURE;
        }
        else
        {
            HTTP_HEADERS_HANDLE request_headers;
            if ((request_headers = construct_http_headers(prov_client, NULL, HTTP_CLIENT_REQUEST_GET)) == NULL)
            {
                LogError("Failure constructing http headers");
                result = MU_FAILURE;
            }
            else
            {
                result = rest_call(prov_client, HTTP_CLIENT_REQUEST_GET, STRING_c_str(registration_path), request_headers, NULL);

                if (result == 0)
                {
                    void* handle;
                    if ((handle = vector.deserializeFromJson(prov_client->response)) == NULL)
                    {
                        LogError("Failure constructing new enrollment structure from json response");
                        result = MU_FAILURE;
                    }
                    *handle_ptr = handle;
                }
                clear_response(prov_client);
            }
            HTTPHeaders_Free(request_headers);
        }
        STRING_delete(registration_path);
    }

    return result;
}

static int prov_sc_run_bulk_operation(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, PROVISIONING_BULK_OPERATION* bulk_op, PROVISIONING_BULK_OPERATION_RESULT** bulk_res_ptr , const char* path_format)
{
    int result = 0;

    if (prov_client == NULL)
    {
        LogError("Invalid Provisioning Client Handle");
        result = MU_FAILURE;
    }
    else if (bulk_op == NULL)
    {
        LogError("Invalid Bulk Op");
        result = MU_FAILURE;
    }
    else if (bulk_op->version != PROVISIONING_BULK_OPERATION_VERSION_1)
    {
        LogError("Invalid Bulk Op Version #");
        result = MU_FAILURE;
    }
    else if (bulk_res_ptr == NULL)
    {
        LogError("Invalid Bulk Op Result pointer");
        result = MU_FAILURE;
    }
    else
    {
        char* content;
        if ((content = bulkOperation_serializeToJson(bulk_op)) == NULL)
        {
            LogError("Failure serializing bulk operation");
            result = MU_FAILURE;
        }
        else
        {
            STRING_HANDLE registration_path = create_registration_path(path_format, NULL);
            if (registration_path == NULL)
            {
                LogError("Failed to construct a registration path");
                result = MU_FAILURE;
            }
            else
            {
                HTTP_HEADERS_HANDLE request_headers;
                if ((request_headers = construct_http_headers(prov_client, NULL, HTTP_CLIENT_REQUEST_POST)) == NULL)
                {
                    LogError("Failure constructing http headers");
                    result = MU_FAILURE;
                }
                else
                {
                    result = rest_call(prov_client, HTTP_CLIENT_REQUEST_POST, STRING_c_str(registration_path), request_headers, content);

                    if (result == 0)
                    {
                        if ((*bulk_res_ptr = bulkOperationResult_deserializeFromJson(prov_client->response)) == NULL)
                        {
                            LogError("Failure deserializing bulk operation result");
                            result = MU_FAILURE;
                        }
                    }
                    else
                    {
                        LogError("Rest call failed");
                    }
                    clear_response(prov_client);
                }
                HTTPHeaders_Free(request_headers);
            }
            STRING_delete(registration_path);
        }
        free(content);
    }

    return result;
}

static int prov_sc_query_records(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, PROVISIONING_QUERY_SPECIFICATION* query_spec, char** cont_token_ptr, PROVISIONING_QUERY_RESPONSE** query_res_ptr, const char* path_format)
{
    int result = 0;

    if (prov_client == NULL)
    {
        LogError("Invalid Provisioning Client Handle");
        result = MU_FAILURE;
    }
    else if (query_spec == NULL || query_spec->version != PROVISIONING_QUERY_SPECIFICATION_VERSION_1)
    {
        LogError("Invalid Query details");
        result = MU_FAILURE;
    }
    else if (cont_token_ptr == NULL)
    {
        LogError("Invalid Continuation Token pointer");
        result = MU_FAILURE;
    }
    else if (query_res_ptr == NULL)
    {
        LogError("Invalid Query Response pointer");
        result = MU_FAILURE;
    }
    else
    {
        char* content = NULL;

        //do not serialize the query specification if there is no query_string (i.e. DRS query)
        if ((query_spec->query_string != NULL) && ((content = querySpecification_serializeToJson(query_spec)) == NULL))
        {
            LogError("Failure serializing query specification");
            result = MU_FAILURE;
        }
        else
        {
            STRING_HANDLE registration_path = create_registration_path(path_format, query_spec->registration_id);
            if (registration_path == NULL)
            {
                LogError("Failed to construct a registration path");
                result = MU_FAILURE;
            }
            else
            {
                HTTP_HEADERS_HANDLE request_headers;
                if ((request_headers = construct_http_headers(prov_client, NULL, HTTP_CLIENT_REQUEST_POST)) == NULL)
                {
                    LogError("Failure constructing http headers");
                    result = MU_FAILURE;
                }
                else if ((add_query_headers(request_headers, query_spec->page_size, *cont_token_ptr)) != 0)
                {
                    LogError("Failure adding query headers");
                    result = MU_FAILURE;
                }
                else
                {
                    result = rest_call(prov_client, HTTP_CLIENT_REQUEST_POST, STRING_c_str(registration_path), request_headers, content);

                    if (result == 0)
                    {
                        const char* resp_type = NULL;
                        char* new_cont_token = NULL;
                        PROVISIONING_QUERY_TYPE type;

                        if (get_response_headers(prov_client, &new_cont_token, &resp_type) != 0)
                        {
                            LogError("Failure reading response headers");
                            result = MU_FAILURE;
                        }
                        else if ((type = queryType_stringToEnum(resp_type)) == QUERY_TYPE_INVALID)
                        {
                            LogError("Failure to parse response type");
                            result = MU_FAILURE;
                        }
                        else if ((*query_res_ptr = queryResponse_deserializeFromJson(prov_client->response, type)) == NULL)
                        {
                            LogError("Failure deserializing query response");
                            result = MU_FAILURE;
                        }
                        free(*cont_token_ptr);
                        *cont_token_ptr = new_cont_token;
                    }
                    else
                    {
                        LogError("Rest call failed");
                    }
                    clear_response(prov_client);
                }
                HTTPHeaders_Free(request_headers);
            }
            STRING_delete(registration_path);
        }
        free(content);
    }

    return result;
}

//Exposed functions below

void prov_sc_destroy(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client)
{
    if (prov_client != NULL)
    {
        free(prov_client->provisioning_service_uri);
        free(prov_client->key_name);
        free(prov_client->access_key);
        free(prov_client->response);
        HTTPHeaders_Free(prov_client->response_headers);
        free(prov_client->certificate);
        free(prov_client);
    }
}

PROVISIONING_SERVICE_CLIENT_HANDLE prov_sc_create_from_connection_string(const char* conn_string)
{
    PROV_SERVICE_CLIENT* result;

    if (conn_string == NULL)
    {
        LogError("Input parameter is NULL: conn_string");
        result = NULL;
    }
    else
    {
        STRING_HANDLE cs_string;
        if ((cs_string = STRING_construct(conn_string)) == NULL)
        {
            LogError("STRING_construct failed");
            result = NULL;
        }
        else
        {
            MAP_HANDLE connection_string_values_map;
            if ((connection_string_values_map = connectionstringparser_parse(cs_string)) == NULL)
            {
                LogError("Tokenizing conn_string failed");
                result = NULL;
            }
            else
            {
                const char* hostname = NULL;
                const char* key_name = NULL;
                const char* key = NULL;

                if ((hostname = Map_GetValueFromKey(connection_string_values_map, IOTHUBHOSTNAME)) == NULL)
                {
                    LogError("Couldn't find %s in conn_string", IOTHUBHOSTNAME);
                    result = NULL;
                }
                else if ((key_name = Map_GetValueFromKey(connection_string_values_map, IOTHUBSHAREDACESSKEYNAME)) == NULL)
                {
                    LogError("Couldn't find %s in conn_string", IOTHUBSHAREDACESSKEYNAME);
                    result = NULL;
                }
                else if ((key = Map_GetValueFromKey(connection_string_values_map, IOTHUBSHAREDACESSKEY)) == NULL)
                {
                    LogError("Couldn't find %s in conn_string", IOTHUBSHAREDACESSKEY);
                    result = NULL;
                }
                if (hostname == NULL || key_name == NULL || key == NULL)
                {
                    LogError("invalid parameter hostname: %p, key_name: %p, key: %p", hostname, key_name, key);
                    result = NULL;
                }
                else if ((result = malloc(sizeof(PROV_SERVICE_CLIENT))) == NULL)
                {
                    LogError("Allocation of provisioning service client failed");
                    result = NULL;
                }
                else
                {
                    memset(result, 0, sizeof(PROV_SERVICE_CLIENT));
                    if (mallocAndStrcpy_s(&result->provisioning_service_uri, hostname) != 0)
                    {
                        LogError("Failure allocating of provisioning service uri");
                        prov_sc_destroy(result);
                        result = NULL;
                    }
                    else if (mallocAndStrcpy_s(&result->key_name, key_name) != 0)
                    {
                        LogError("Failure allocating of keyname");
                        prov_sc_destroy(result);
                        result = NULL;
                    }
                    else if (mallocAndStrcpy_s(&result->access_key, key) != 0)
                    {
                        LogError("Failure allocating of access key");
                        prov_sc_destroy(result);
                        result = NULL;
                    }
                    else
                    {
                        result->tracing = TRACING_STATUS_OFF;
                    }
                }
                Map_Destroy(connection_string_values_map);
            }
        }
        STRING_delete(cs_string);
    }
    return result;
}

void prov_sc_set_trace(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, TRACING_STATUS status)
{
    if (prov_client != NULL)
    {
        prov_client->tracing = status;
    }
}

int prov_sc_set_certificate(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, const char* certificate)
{
    int result = 0;

    if (prov_client == NULL)
    {
        LogError("Invalid prov_client");
        result = MU_FAILURE;
    }
    else if (certificate == NULL)
    {
        free(prov_client->certificate);
        prov_client->certificate = NULL;
    }
    else if (mallocAndStrcpy_overwrite(&prov_client->certificate, (char*)certificate) != 0)
    {
        LogError("Failed allocating memory for certificate");
        result = MU_FAILURE;
    }

    return result;
}

int prov_sc_set_proxy(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, HTTP_PROXY_OPTIONS* proxy_options)
{
    int result = 0;

    if (prov_client == NULL)
    {
        LogError("Invalid prov_client");
        result = MU_FAILURE;
    }
    else if (proxy_options == NULL)
    {
        LogError("Invalid proxy options");
        result = MU_FAILURE;
    }
    else
    {
        if (proxy_options->host_address == NULL)
        {
            LogError("Null host address in proxy options");
            result = MU_FAILURE;
        }
        else if (((proxy_options->username == NULL) || (proxy_options->password == NULL))
            && (proxy_options->username != proxy_options->password))
        {
            LogError("Only one of username and password for proxy settings was NULL");
            result = MU_FAILURE;
        }
        else
        {
            prov_client->proxy_options = proxy_options;
        }
    }

    return result;
}

int prov_sc_create_or_update_individual_enrollment(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, INDIVIDUAL_ENROLLMENT_HANDLE* enrollment_ptr)
{
    return prov_sc_create_or_update_record(prov_client,(void**)enrollment_ptr, getVector_individualEnrollment(), INDV_ENROLL_PROVISION_PATH_FMT);
}

int prov_sc_delete_individual_enrollment(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, INDIVIDUAL_ENROLLMENT_HANDLE enrollment)
{
    const char* eTag = individualEnrollment_getEtag(enrollment);
    const char* registrationId = individualEnrollment_getRegistrationId(enrollment);
    return prov_sc_delete_record_by_param(prov_client, registrationId, eTag, INDV_ENROLL_PROVISION_PATH_FMT);
}

int prov_sc_delete_individual_enrollment_by_param(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, const char* reg_id, const char* etag)
{
    return prov_sc_delete_record_by_param(prov_client, reg_id, etag, INDV_ENROLL_PROVISION_PATH_FMT);
}

int prov_sc_get_individual_enrollment(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, const char* reg_id, INDIVIDUAL_ENROLLMENT_HANDLE* enrollment_ptr)
{
    return prov_sc_get_record(prov_client, reg_id, (void**)enrollment_ptr, getVector_individualEnrollment(), INDV_ENROLL_PROVISION_PATH_FMT);
}

int prov_sc_query_individual_enrollment(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, PROVISIONING_QUERY_SPECIFICATION* query_spec, char** cont_token_ptr, PROVISIONING_QUERY_RESPONSE** query_resp_ptr)
{
    return prov_sc_query_records(prov_client, query_spec, cont_token_ptr, query_resp_ptr, INDV_ENROLL_QUERY_PATH_FMT);
}

int prov_sc_run_individual_enrollment_bulk_operation(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, PROVISIONING_BULK_OPERATION* bulk_op, PROVISIONING_BULK_OPERATION_RESULT** bulk_res_ptr)
{
    return prov_sc_run_bulk_operation(prov_client, bulk_op, bulk_res_ptr, INDV_ENROLL_BULK_PATH_FMT);
}

int prov_sc_delete_device_registration_state(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, DEVICE_REGISTRATION_STATE_HANDLE reg_state)
{
    const char* eTag = deviceRegistrationState_getEtag(reg_state);
    const char* registrationId = deviceRegistrationState_getRegistrationId(reg_state);
    return prov_sc_delete_record_by_param(prov_client, registrationId, eTag, REG_STATE_PROVISION_PATH_FMT);
}

int prov_sc_delete_device_registration_state_by_param(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, const char* reg_id, const char* etag)
{
    return prov_sc_delete_record_by_param(prov_client, reg_id, etag, REG_STATE_PROVISION_PATH_FMT);
}

int prov_sc_get_device_registration_state(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, const char* reg_id, DEVICE_REGISTRATION_STATE_HANDLE* reg_state_ptr)
{
    return prov_sc_get_record(prov_client, reg_id, (void**)reg_state_ptr, getVector_registrationState(), REG_STATE_PROVISION_PATH_FMT);
}

int prov_sc_query_device_registration_state(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, PROVISIONING_QUERY_SPECIFICATION* query_spec, char** cont_token_ptr, PROVISIONING_QUERY_RESPONSE** query_resp_ptr)
{
    return prov_sc_query_records(prov_client, query_spec, cont_token_ptr, query_resp_ptr, REG_STATE_QUERY_PATH_FMT);
}

int prov_sc_create_or_update_enrollment_group(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, ENROLLMENT_GROUP_HANDLE* enrollment_ptr)
{
    return prov_sc_create_or_update_record(prov_client, (void**)enrollment_ptr, getVector_enrollmentGroup(), ENROLL_GROUP_PROVISION_PATH_FMT);
}

int prov_sc_delete_enrollment_group(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, ENROLLMENT_GROUP_HANDLE enrollment)
{
    const char* eTag = enrollmentGroup_getEtag(enrollment);
    const char* groupId = enrollmentGroup_getGroupId(enrollment);
    return prov_sc_delete_record_by_param(prov_client, groupId, eTag, ENROLL_GROUP_PROVISION_PATH_FMT);
}

int prov_sc_delete_enrollment_group_by_param(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, const char* group_id, const char* etag)
{
    return prov_sc_delete_record_by_param(prov_client, group_id, etag, ENROLL_GROUP_PROVISION_PATH_FMT);
}

int prov_sc_get_enrollment_group(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, const char* group_id, ENROLLMENT_GROUP_HANDLE* enrollment_ptr)
{
    return prov_sc_get_record(prov_client, group_id, (void**)enrollment_ptr, getVector_enrollmentGroup(), ENROLL_GROUP_PROVISION_PATH_FMT);
}

int prov_sc_query_enrollment_group(PROVISIONING_SERVICE_CLIENT_HANDLE prov_client, PROVISIONING_QUERY_SPECIFICATION* query_spec, char** cont_token_ptr, PROVISIONING_QUERY_RESPONSE** query_resp_ptr)
{
    return prov_sc_query_records(prov_client, query_spec, cont_token_ptr, query_resp_ptr, ENROLL_GROUP_QUERY_PATH_FMT);
}