/*
 * Copyright (C) 2020 MERA
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "AzureClient.h"

#include <iothub.h>
#include <iothub_device_client.h>
#include <iothub_client_options.h>
#include <iothub_message.h>
#include <iothubtransportmqtt.h>
#include <azure_c_shared_utility/threadapi.h> // ThreadAPI_Sleep()
#include <azure_c_shared_utility/tickcounter.h> // tickcounter_ms_t

#include "utils.h"
#include "CloudType.h"
#include "ClientManager.h"

#include <glib.h>
#include <afb/afb-binding.h> // for AFB_* logger

namespace
{

void connection_status_callback(IOTHUB_CLIENT_CONNECTION_STATUS result, IOTHUB_CLIENT_CONNECTION_STATUS_REASON reason, void* user_context)
{
    AFB_NOTICE("%s called: result %d, reason %d", __FUNCTION__, result, reason);

    (void)reason;
    (void)user_context;
    // This sample DOES NOT take into consideration network outages.
    if (result == IOTHUB_CLIENT_CONNECTION_AUTHENTICATED && reason == IOTHUB_CLIENT_CONNECTION_OK)
    {
        AFB_NOTICE("The device client is connected to iothub");
    }
    else
    {
        AFB_NOTICE("The device client has been disconnected");
    }
}

IOTHUBMESSAGE_DISPOSITION_RESULT receive_msg_callback(IOTHUB_MESSAGE_HANDLE message, void* user_context)
{
    AFB_NOTICE("%s called", __FUNCTION__);
    (void)user_context;

    IOTHUBMESSAGE_CONTENT_TYPE content_type = IoTHubMessage_GetContentType(message);

    if (content_type == IOTHUBMESSAGE_BYTEARRAY)
    {
        const unsigned char* buff_msg;
        size_t buff_len;

        if (IoTHubMessage_GetByteArray(message, &buff_msg, &buff_len) != IOTHUB_MESSAGE_OK)
        {
            AFB_ERROR("Failure retrieving byte array message");
        }
        else
        {
            AFB_NOTICE("Received Binary message, size %d, data '%.*s'", (int)buff_len, (int)buff_len, buff_msg);
        }

        const char* app_id = IoTHubMessage_GetProperty(message, "application_id");
        AFB_NOTICE("Received property 'application_id': %s", (app_id ? app_id : "<unavailable>"));

        if (app_id && app_id[0])
            ClientManager::instance().emitReceivedMessage(app_id, CloudType::Azure, std::string((const char*)buff_msg, buff_len));
        else
            AFB_ERROR("Can't emit SendMessageConfirmation: appid is not valid");
    }
    else if (content_type == IOTHUBMESSAGE_STRING)
    {
        const char* string_msg = IoTHubMessage_GetString(message);
        if (string_msg == nullptr)
        {
            AFB_NOTICE("Failure retrieving String message");
        }
        else
        {
            AFB_NOTICE("Received String message, size %lu, data '%s'", strlen(string_msg), string_msg);
        }

        const char* app_id = IoTHubMessage_GetProperty(message, "application_id");
        AFB_NOTICE("Received property 'application_id': %s", (app_id ? app_id : "<unavailable>"));

        if (app_id && app_id[0])
            ClientManager::instance().emitReceivedMessage(app_id, CloudType::Azure, string_msg);
        else
            AFB_ERROR("Can't emit SendMessageConfirmation: appid is not valid");
    }
    else
    {
        AFB_ERROR("Unsupported message content type");
    }

    return IOTHUBMESSAGE_ACCEPTED;
}


int device_method_callback(const char* method_name, const unsigned char* payload, size_t size, unsigned char** response, size_t* resp_size, void* userContextCallback)
{
    AFB_NOTICE("%s called, method_name %s", __FUNCTION__, method_name);

    const char* device_id = (const char*)userContextCallback;

    int status = 501;
    const char* RESPONSE_STRING = "{ \"Response\": \"Unknown method requested.\" }";

    AFB_NOTICE("Device Method called for device %s", device_id);
    AFB_NOTICE("Device Method name:    %s", method_name);
    AFB_NOTICE("Device Method payload: %.*s", (int)size, (const char*)payload);


    AFB_NOTICE("Response status: %d", status);
    AFB_NOTICE("Response payload: %s", RESPONSE_STRING);

    *resp_size = strlen(RESPONSE_STRING);
    if ((*response = (unsigned char*)malloc(*resp_size)) == nullptr)
    {
        status = -1;
    }
    else
    {
        memcpy(*response, RESPONSE_STRING, *resp_size);
    }

    return status;
}


void send_confirm_callback(IOTHUB_CLIENT_CONFIRMATION_RESULT result, void* userContextCallback)
{
    AFB_NOTICE("%s called, result %d", __FUNCTION__, result);
    (void)userContextCallback;
    // When a message is sent this callback will get invoked

    AFB_NOTICE("Confirmation callback result %s", MU_ENUM_TO_STRING(IOTHUB_CLIENT_CONFIRMATION_RESULT, result));

    const char* appid = (const char*)userContextCallback;
    if (!appid || !appid[0])
    {
        AFB_ERROR("Confirmation callback: appid is not set");

        if (userContextCallback)
            free(userContextCallback);

        return;
    }

    ClientManager::instance().emitSendMessageConfirmation(appid, CloudType::Azure, result == IOTHUB_CLIENT_CONFIRMATION_OK);
    free(userContextCallback);
}

} //end namespace

AzureClient::AzureClient() = default;

AzureClient::~AzureClient()
{
    if (m_iot_inited)
    {
        if (m_azure_client && *m_azure_client)
            IoTHubDeviceClient_Destroy(*m_azure_client);

        IoTHub_Deinit();
    }
}

bool AzureClient::createConnection()
{
    AFB_NOTICE("%s called", __FUNCTION__);

    if (m_iot_inited)
    {
        AFB_ERROR("Azure IoT already initalized");
        return false;
    }

    // Init Azure API:
    {
        int res = IoTHub_Init();
        m_iot_inited = true;

        if (res)
        {
            AFB_ERROR("Azure IoTHub_Init() failed: %d", res);
            return false;
        }
    }

    if (m_azure_client)
    {
        AFB_ERROR("connection already created");
        return false;
    }

    IOTHUB_DEVICE_CLIENT_HANDLE device_handle = IoTHubDeviceClient_CreateFromConnectionString(m_conf.device_connection_string.c_str(), MQTT_Protocol);
    if (!device_handle)
    {
        AFB_ERROR("Failure creating Azure IoTHubDeviceClient device");
        return false;
    }

    bool traceOn = false;
    IoTHubDeviceClient_SetOption(device_handle, OPTION_LOG_TRACE, &traceOn);
    IoTHubDeviceClient_SetConnectionStatusCallback(device_handle, connection_status_callback, nullptr);
    IoTHubDeviceClient_SetMessageCallback(device_handle, receive_msg_callback, nullptr);
    IoTHubDeviceClient_SetDeviceMethodCallback(device_handle, device_method_callback, nullptr);

    tickcounter_ms_t ms_delay = 10;
    IoTHubDeviceClient_SetOption(device_handle, OPTION_DO_WORK_FREQUENCY_IN_MS, &ms_delay); // DoWork multithread

    m_azure_client.reset(new IOTHUB_DEVICE_CLIENT_HANDLE{device_handle});

    return true;
}


bool AzureClient::sendMessage(const std::string& appid, const std::string& data)
{
    if (!m_azure_client)
    {
        AFB_ERROR("AzureClient is not ready for message sending");
        return false;
    }

    IOTHUB_MESSAGE_HANDLE message_handle = IoTHubMessage_CreateFromString(data.c_str());

    utils::scope_exit message_handle_destroy([&message_handle](){
        // The message is copied to the sdk, so the we can destroy it
        if (message_handle)
            IoTHubMessage_Destroy(message_handle);
    });

    if (!message_handle)
    {
        AFB_ERROR("Can't create IoTHubMessage message");
        return false;
    }

    IoTHubMessage_SetProperty(message_handle, "application_id", appid.c_str());

    if (IoTHubDeviceClient_SendEventAsync(*m_azure_client, message_handle, send_confirm_callback, strdup(appid.c_str())))
    {
        AFB_ERROR("Can't send IoTHubMessage message");
        return false;
    }

    return true;
}

bool AzureClient::enabled() const
{
    return m_conf.enabled;
}

bool AzureClient::connected() const
{
    return (m_azure_client && *m_azure_client);
}

bool AzureClient::loadConf(GKeyFile* conf_file)
{
    g_autoptr(GError) error = nullptr;

    // Azure parameters:
    m_conf.enabled = g_key_file_get_boolean(conf_file, "AzureCloudConnection", "Enabled", &error);

    g_autofree gchar *value = g_key_file_get_string(conf_file, "AzureCloudConnection", "DeviceConnectionString", &error);
    if (value == nullptr)
    {
        AFB_ERROR("can't read AzureCloudConnection/DeviceConnectionString from config");
        return false;
    }

    m_conf.device_connection_string = value;
    if (m_conf.device_connection_string.empty())
    {
        AFB_ERROR("AzureCloudConnection/DeviceConnectionString is empty");
        return false;
    }

    return true;
}