/*
 * 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 <afb/afb-binding.h>
#include <json-c/json.h>

#include <memory>
#include <algorithm>

#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 "hmi-debug.h"
#include "utils.h"
#include "ClientManager.h"

#include <glib.h>

static const char* API_name{"cloudproxy"};

static std::string g_connectionString;
static IOTHUB_DEVICE_CLIENT_HANDLE g_device_handle{nullptr};
static bool g_iot_inited{false};



static utils::scope_exit g_destroy([]{
    if (g_iot_inited)
    {
        if (g_device_handle)
            IoTHubDeviceClient_Destroy(g_device_handle);

        IoTHub_Deinit();
    }
});


static bool loadConf()
{
    const char* CONF_FILE_PATH{"/etc/config.ini"};
    const char* DEFAULT_ETC_PATH{"/etc/cloudproxy-service"};
    const char *p = getenv("AFM_APP_INSTALL_DIR");
    if(!p)
    {
        HMI_ERROR("cloudproxy-service", "AFM_APP_INSTALL_DIR is not set, try to find conf in %s", DEFAULT_ETC_PATH);
        p = DEFAULT_ETC_PATH;
    }

    std::string conf_path = p;
    conf_path += CONF_FILE_PATH;

    g_autoptr(GKeyFile) conf_file = g_key_file_new();
    g_autoptr(GError) error = nullptr;

    if (!conf_file || !g_key_file_load_from_file(conf_file, conf_path.c_str(), G_KEY_FILE_NONE, &error))
    {
        HMI_ERROR("cloudproxy-service", "can't load file %s", conf_path.c_str());
        return false;
    }

    g_autofree gchar *value = g_key_file_get_string(conf_file, "AzureCloudConnection", "DeviceConnectionString", &error);
    if (value == nullptr)
    {
        HMI_ERROR("cloudproxy-service", "can't read DeviceConnectionString from config %d", conf_path.c_str());
        return false;
    }

    g_connectionString = value;
    if (g_connectionString.empty())
    {
        HMI_ERROR("cloudproxy-service", "DeviceConnectionString is empty");
        return false;
    }

    return true;
}


//--------------  Iot callbacks
static void connection_status_callback(IOTHUB_CLIENT_CONNECTION_STATUS result, IOTHUB_CLIENT_CONNECTION_STATUS_REASON reason, void* user_context)
{
    HMI_NOTICE("cloudproxy-service", "%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)
    {
        HMI_NOTICE("cloudproxy-service", "The device client is connected to iothub");
    }
    else
    {
        HMI_NOTICE("cloudproxy-service", "The device client has been disconnected");
    }
}

static IOTHUBMESSAGE_DISPOSITION_RESULT receive_msg_callback(IOTHUB_MESSAGE_HANDLE message, void* user_context)
{
    HMI_NOTICE("cloudproxy-service", "%s called", __FUNCTION__);
    (void)user_context;
    const char* messageId;
    const char* correlationId;

    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)
        {
            HMI_ERROR("cloudproxy-service", "Failure retrieving byte array message");
        }
        else
        {
            HMI_NOTICE("cloudproxy-service", "Received Binary message, size %d, data '%.*s'", (int)buff_len, (int)buff_len, buff_msg);
        }

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

        if (app_id && app_id[0])
            ClientManager::instance().emitReceivedMessage(app_id, std::string((const char*)buff_msg, buff_len));
        else
            HMI_ERROR("cloudproxy-service", "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)
        {
            HMI_NOTICE("cloudproxy-service", "Failure retrieving String message");
        }
        else
        {
            HMI_NOTICE("cloudproxy-service", "Received String message, size %d, data '%s'", strlen(string_msg), string_msg);
        }

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

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

    return IOTHUBMESSAGE_ACCEPTED;
}


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

    const char* device_id = (const char*)userContextCallback;
    char* end = nullptr;
    int newInterval;

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

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

    HMI_NOTICE("cloudproxy-service", "Response status: %d", status);
    HMI_NOTICE("cloudproxy-service", "Response payload: %s", RESPONSE_STRING);

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

    return status;
}


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

    HMI_NOTICE("cloudproxy-service", "Confirmation callback result %s", MU_ENUM_TO_STRING(IOTHUB_CLIENT_CONFIRMATION_RESULT, result));

    const char* appid = (const char*)userContextCallback;
    if (!appid || !appid[0])
    {
        HMI_ERROR("cloudproxy-service", "Confirmation callback: appid is not set");

        if (userContextCallback)
            free(userContextCallback);

        return;
    }

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

//-------------- help functions
static bool createConnection()
{
    HMI_NOTICE("cloudproxy-service", "%s called", __FUNCTION__);

    if (g_device_handle)
    {
        HMI_WARNING("cloudproxy-service", "connection already created");
        return true;
    }

    g_device_handle = IoTHubDeviceClient_CreateFromConnectionString(g_connectionString.c_str(), MQTT_Protocol);
    if (!g_device_handle)
    {
        HMI_ERROR("cloudproxy-service", "Failure creating IoTHubDeviceClient device");
        return false;
    }

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

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

    return true;
}

//--------------


static void pingSample(afb_req_t request)
{
   static int pingcount = 0;
   afb_req_success_f(request, json_object_new_int(pingcount), "Ping count = %d", pingcount);
   HMI_NOTICE("cloudproxy-service", "Verbosity macro at level notice invoked at ping invocation count = %d", pingcount);
   pingcount++;
}


static bool initAzureSdk()
{
    //Allow program to try to establish connection several times
    if (!g_iot_inited)
    {
        if(IoTHub_Init())
        {
            HMI_ERROR("cloudproxy-service","Azure IoTHub_Init() failed");
        }
        else
        {
            g_iot_inited = true;
        }
    }

    return g_iot_inited;
}

static void sendMessage(afb_req_t request)
{
    HMI_NOTICE("cloudproxy-service", "%s called", __FUNCTION__);

    json_object* object = afb_req_json(request);
    if (!object)
    {
        HMI_ERROR("cloudproxy-service", "Can't parse request");
        afb_req_fail_f(request, "failed", "called %s", __FUNCTION__);
        return;
    }

    const std::string appid{utils::get_application_id(request)};
    std::string data;
    json_object *obj_data;
    if(!json_object_object_get_ex(object, "data", &obj_data))
    {
        HMI_ERROR("cloudproxy-service", "can't obtain application_id or data from request");
        return;
    }
    data = json_object_get_string(obj_data);

    if (!g_device_handle && !createConnection())
    {
        HMI_ERROR("cloudproxy-service", "Can't create connection to cloud");
        afb_req_fail_f(request, "failed", "called %s", __FUNCTION__);
        return;
    }

    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)
    {
        HMI_ERROR("cloudproxy-service", "Can't create IoTHubMessage message");
        afb_req_fail_f(request, "failed", "called %s", __FUNCTION__);
        return;
    }

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

    if (IoTHubDeviceClient_SendEventAsync(g_device_handle, message_handle, send_confirm_callback, strdup(appid.c_str())))
    {
        HMI_ERROR("cloudproxy-service", "Can't send IoTHubMessage message");
        afb_req_fail_f(request, "failed", "called %s", __FUNCTION__);
        return;
    }

    afb_req_success(request, json_object_new_object(), __FUNCTION__);
}


static void subscribe(afb_req_t request)
{
    HMI_NOTICE("cloudproxy-service", "%s called", __FUNCTION__);

    std::string req_appid{utils::get_application_id(request)};
    if(req_appid.empty())
    {
        HMI_ERROR("cloudproxy-service", "Can't subscribe: empty appid");
        afb_req_fail_f(request, "%s failed: application_id is not defined in request", __FUNCTION__);
        return;
    }

    if (!ClientManager::instance().handleRequest(request, __FUNCTION__, req_appid))
    {
        HMI_ERROR("cloudproxy-service", "%s failed in handleRequest", __FUNCTION__);
        afb_req_fail_f(request, "%s failed", __FUNCTION__);
    }
    else
    {
        afb_req_success(request, json_object_new_object(), __FUNCTION__);
    }
}

static void unsubscribe(afb_req_t request)
{
    HMI_NOTICE("cloudproxy-service", "%s called", __FUNCTION__);

    std::string req_appid{utils::get_application_id(request)};
    if(req_appid.empty())
    {
        HMI_ERROR("cloudproxy-service", "Can't unsubscribe: empty appid");
        afb_req_fail_f(request, "%s failed: application_id is not defined in request", __FUNCTION__);
        return;
    }

    if (!ClientManager::instance().handleRequest(request, __FUNCTION__, req_appid))
    {
        HMI_ERROR("cloudproxy-service", "%s failedin handleRequest", __FUNCTION__);
        afb_req_fail_f(request, "%s failed", __FUNCTION__);
    }
    else
    {
        afb_req_success(request, json_object_new_object(), __FUNCTION__);
    }
}

/*
 * array of the verbs exported to afb-daemon
 */
static const afb_verb_t verbs[]= {
    /* VERB'S NAME                 FUNCTION TO CALL                  */
    { .verb="ping",              .callback=pingSample             },
    { .verb="sendMessage",       .callback=sendMessage            },
    { .verb="subscribe",         .callback=subscribe              },
    { .verb="unsubscribe",       .callback=unsubscribe            },
    {nullptr } /* marker for end of the array */
};


static int preinit(afb_api_t api)
{
    HMI_NOTICE("cloudproxy-service", "binding preinit (was register)");

    if (!loadConf())
    {
        HMI_ERROR("cloudproxy-service", "Can't load configuration file or configuration is wrong");
        return -1;
    }

    if (!initAzureSdk())
    {
        HMI_ERROR("cloudproxy-service", "Can't initialize Azure SDK");
        return -1;
    }

    return 0;
}


static int init(afb_api_t api)
{
    HMI_NOTICE("cloudproxy-service","binding init");
    return (g_iot_inited ? 0 : -1);
}

const afb_binding_t afbBindingExport = {
    .api = "cloudproxy",
    .specification = nullptr,
    .info = nullptr,
    .verbs = verbs,
    .preinit = preinit,
    .init = init,
    .onevent = nullptr
};