/*
 * Authors:
 * Roman Khlopkov <roman.khlopkov@demlabs.net>
 * Dmitriy Gerasimov <dmitriy.gerasmiov@demlabs.net>
 * DeM Labs Ltd   https://demlabs.net
 * DeM Labs Open source community https://gitlab.demlabs.net
 * Copyright  (c) 2021
 * All rights reserved.

 This file is part of DapChain SDK the open source project

    DapChain SDK is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    DapChain SDK is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with any DapChain SDK based project.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "dap_net.h"
#include "dap_events.h"
#include "dap_context.h"
#include "dap_timerfd.h"
#include "dap_chain_node_dns_server.h"
#include "dap_chain_node_dns_client.h"

#define LOG_TAG "dap_chain_node_dns_client"

struct dns_client
{
    dap_events_socket_t * parent;
    struct in_addr addr;
    uint16_t port;
    char *name;
    dap_dns_buf_t dns_request;
    byte_t buf[1024];

    dap_dns_client_node_info_request_success_callback_t callback_success;
    dap_dns_client_node_info_request_error_callback_t callback_error;
    void * callbacks_arg;

    bool is_callbacks_called;
};

static void s_dns_client_esocket_read_callback(dap_events_socket_t * a_esocket, void * a_arg);
static void s_dns_client_esocket_error_callback(dap_events_socket_t * a_esocket, int a_error);
static bool s_dns_client_esocket_timeout_callback( void * a_arg);
static void s_dns_client_esocket_delete_callback(dap_events_socket_t * a_esocket, void * a_arg);
static void s_dns_client_esocket_worker_assign_callback(dap_events_socket_t * a_esocket, dap_worker_t * a_worker);

/**
 * @brief s_dns_client_esocket_read_callback
 * @param a_esocket
 * @param a_arg
 */
static void s_dns_client_esocket_read_callback(dap_events_socket_t * a_esocket, void * a_arg)
{
    (void) a_arg;
    struct dns_client * l_dns_client = (struct dns_client*) a_esocket->_inheritor;
    byte_t * l_buf = a_esocket->buf_in;
    size_t l_recieved = a_esocket->buf_in_size;
    size_t l_addr_point = DNS_HEADER_SIZE + strlen(l_dns_client->name) + 2 + 2 * sizeof(uint16_t) + DNS_ANSWER_SIZE - sizeof(uint32_t);
    if (l_recieved < l_addr_point + sizeof(uint32_t)) {
        log_it(L_WARNING, "DNS answer incomplete");
        l_dns_client->callback_error(a_esocket->worker, l_dns_client->callbacks_arg, EIO);
        l_dns_client->is_callbacks_called = true;
        a_esocket->flags |= DAP_SOCK_SIGNAL_CLOSE;
        a_esocket->buf_in_size = a_esocket->buf_out_size = 0;
        return;
    }
    byte_t * l_cur = l_buf + 3 * sizeof(uint16_t);
    int l_answers_count = ntohs(*(uint16_t *)l_cur);
    if (l_answers_count != 1) {
        log_it(L_WARNING, "Incorrect DNS answer format");
        l_dns_client->callback_error(a_esocket->worker, l_dns_client->callbacks_arg, EINVAL);
        l_dns_client->is_callbacks_called = true;
        a_esocket->flags |= DAP_SOCK_SIGNAL_CLOSE;
        a_esocket->buf_in_size = a_esocket->buf_out_size = 0;
        return;
    }
    l_cur = l_buf + l_addr_point;

    dap_chain_net_links_t *l_link_full_node_list = DAP_NEW_Z_SIZE(dap_chain_net_links_t,
                                                                          sizeof(dap_chain_net_links_t) + sizeof(dap_chain_node_info_t));
    dap_chain_node_info_t l_result = {};
    //l_result.hdr.ext_addr_v4.s_addr = ntohl(*(uint32_t *)l_cur); TODO: implement other request and response
    l_cur = l_buf + 5 * sizeof(uint16_t);
    int l_additions_count = ntohs(*(uint16_t *)l_cur);
    if (l_additions_count == 1) {
        l_cur = l_buf + l_addr_point + DNS_ANSWER_SIZE;
        l_result.ext_port = ntohs(*(uint16_t *)l_cur);
        l_cur += sizeof(uint16_t);
        l_result.address.uint64 = be64toh(*(uint64_t *)l_cur);
    }
    *(dap_chain_node_info_t*)l_link_full_node_list->nodes_info = l_result;
    l_link_full_node_list->count_node = 1;

    l_dns_client->callback_success(a_esocket->worker, l_link_full_node_list, l_dns_client->callbacks_arg);
    l_dns_client->is_callbacks_called = true;
    a_esocket->flags |= DAP_SOCK_SIGNAL_CLOSE;
    a_esocket->buf_in_size = a_esocket->buf_out_size = 0;
    DAP_DELETE(l_link_full_node_list);
}

/**
 * @brief s_dns_client_esocket_error_callback
 * @param a_esocket
 * @param a_error
 */
static void s_dns_client_esocket_error_callback(dap_events_socket_t * a_esocket, int a_error)
{
    struct dns_client * l_dns_client = (struct dns_client*) a_esocket->_inheritor;
    log_it(L_ERROR,"DNS client esocket error %d", a_error);
    l_dns_client->callback_error(a_esocket->worker, l_dns_client->callbacks_arg, a_error);
    l_dns_client->is_callbacks_called = true;
}

/**
 * @brief s_dns_client_esocket_timeout_callback
 * @param a_worker
 * @param a_arg
 * @return
 */
static bool s_dns_client_esocket_timeout_callback(void * a_arg)
{
    assert(a_arg);
    dap_events_socket_uuid_t * l_es_uuid_ptr = (dap_events_socket_uuid_t *) a_arg;
    assert(l_es_uuid_ptr);


    dap_worker_t * l_worker = dap_worker_get_current(); // We're in own esocket context
    assert(l_worker);

    dap_events_socket_t * l_es;
    if((l_es = dap_context_find(l_worker->context,*l_es_uuid_ptr) ) != NULL){ // If we've not closed this esocket
        struct dns_client * l_dns_client = (struct dns_client*) l_es->_inheritor;
        log_it(L_WARNING,"DNS request timeout, bad network?");
        if(! l_dns_client->is_callbacks_called ){
            l_dns_client->callback_error(l_es->worker, l_dns_client->callbacks_arg, ETIMEDOUT);
            l_dns_client->is_callbacks_called = true;
        }
        dap_events_socket_remove_and_delete_unsafe( l_es, false);
    }
    DAP_DEL_Z(l_es_uuid_ptr);
    return false;
}

/**
 * @brief s_dns_client_esocket_delete_callback
 * @param a_esocket
 * @param a_arg
 */
static void s_dns_client_esocket_delete_callback(dap_events_socket_t * a_esocket, void * a_arg)
{
    (void) a_arg;
    struct dns_client * l_dns_client = (struct dns_client*) a_esocket->_inheritor;
    if(! l_dns_client->is_callbacks_called )
        l_dns_client->callback_error(a_esocket->worker, l_dns_client->callbacks_arg, EBUSY);
    if(l_dns_client->name)
        DAP_DELETE(l_dns_client->name);
}

/**
 * @brief s_dns_client_esocket_worker_assign_callback
 * @param a_esocket
 * @param a_worker
 */
static void s_dns_client_esocket_worker_assign_callback(dap_events_socket_t * a_esocket, dap_worker_t * a_worker)
{
    struct dns_client * l_dns_client = (struct dns_client*) a_esocket->_inheritor;
    dap_events_socket_write_unsafe(a_esocket,l_dns_client->dns_request.data, l_dns_client->dns_request.size );

    dap_events_socket_uuid_t * l_es_uuid_ptr = DAP_NEW_Z(dap_events_socket_uuid_t);
    if (!l_es_uuid_ptr) {
        log_it(L_CRITICAL, "%s", g_error_memory_alloc);
        return;
    }
    *l_es_uuid_ptr = a_esocket->uuid;
    dap_timerfd_start_on_worker(a_worker, dap_config_get_item_uint64_default(g_config,"dns_client","request_timeout",10)*1000,
                                 s_dns_client_esocket_timeout_callback,l_es_uuid_ptr);

}

/**
 * @brief dap_chain_node_info_dns_request
 * @param a_addr
 * @param a_port
 * @param a_name
 * @param a_result
 * @param a_callback_success
 * @param a_callback_error
 * @param a_callbacks_arg
 */
int dap_chain_node_info_dns_request(dap_worker_t *a_worker, struct in_addr a_addr, uint16_t a_port, char *a_name,
                           dap_dns_client_node_info_request_success_callback_t a_callback_success,
                           dap_dns_client_node_info_request_error_callback_t a_callback_error,void * a_callbacks_arg)
{
    char l_addr_str[INET_ADDRSTRLEN] = {};
    inet_ntop(AF_INET, &a_addr, l_addr_str, INET_ADDRSTRLEN);

    log_it(L_INFO, "DNS request for bootstrap nodelist  %s : %d, net %s", l_addr_str, a_port, a_name);

    struct dns_client * l_dns_client = DAP_NEW_Z(struct dns_client);
    if(!l_dns_client)
        return -1;
    l_dns_client->name = dap_strdup(a_name);
    l_dns_client->callback_error = a_callback_error;
    l_dns_client->callback_success = a_callback_success;
    l_dns_client->callbacks_arg = a_callbacks_arg;
    l_dns_client->addr = a_addr;
    dap_dns_buf_init(&l_dns_client->dns_request, (char *)l_dns_client->buf);
    dap_dns_buf_put_uint16(&l_dns_client->dns_request, rand() % 0xFFFF);    // ID


    dap_dns_message_flags_t l_flags = {};
    dap_dns_buf_put_uint16(&l_dns_client->dns_request, l_flags.val);
    dap_dns_buf_put_uint16(&l_dns_client->dns_request, 1);                  // we have only 1 question
    dap_dns_buf_put_uint16(&l_dns_client->dns_request, 0);
    dap_dns_buf_put_uint16(&l_dns_client->dns_request, 0);
    dap_dns_buf_put_uint16(&l_dns_client->dns_request, 0);

    size_t l_ptr = 0;
    uint8_t *l_cur = l_dns_client->buf + l_dns_client->dns_request.size;
    size_t l_name_len = strlen(a_name);
    for (size_t i = 0; i <= l_name_len; i++)
        if (a_name[i] == '.' || a_name[i] == 0) {
            *l_cur++ = i - l_ptr;
            while (l_ptr < i)
                *l_cur++ = a_name[l_ptr++];
            l_ptr++;
        }
    *l_cur++='\0';
    l_dns_client->dns_request.size = l_cur - l_dns_client->buf;
    dap_dns_buf_put_uint16(&l_dns_client->dns_request, DNS_RECORD_TYPE_A);
    dap_dns_buf_put_uint16(&l_dns_client->dns_request, DNS_CLASS_TYPE_IN);

    dap_events_socket_callbacks_t l_esocket_callbacks={};

    l_esocket_callbacks.worker_assign_callback = s_dns_client_esocket_worker_assign_callback;
    l_esocket_callbacks.delete_callback = s_dns_client_esocket_delete_callback; // Delete client callback
    l_esocket_callbacks.read_callback = s_dns_client_esocket_read_callback; // Read function
    l_esocket_callbacks.error_callback = s_dns_client_esocket_error_callback; // Error processing function

    dap_events_socket_t * l_esocket = dap_events_socket_create(DESCRIPTOR_TYPE_SOCKET_UDP,&l_esocket_callbacks);
    /*l_esocket->remote_addr.sin_family = AF_INET;
    l_esocket->remote_addr.sin_port = htons(a_port);
    l_esocket->remote_addr.sin_addr = a_addr;
    l_esocket->_inheritor = l_dns_client;*/ // TODO fill the address from config string with no passing it as arg...

    dap_events_socket_assign_on_worker_mt(l_esocket, a_worker);
    return 0;
}


/**
 * @brief dap_dns_buf_init Initialize DNS parser buffer
 * @param buf DNS buffer structure
 * @param msg DNS message
 * @return none
 */
void dap_dns_buf_init(dap_dns_buf_t *buf, char *msg)
{
    buf->data = msg;
    buf->size = 0;
}

/**
 * @brief dap_dns_buf_get_uint16 Get uint16 from network order
 * @param buf DNS buffer structure
 * @return uint16 in host order
 */
uint16_t dap_dns_buf_get_uint16(dap_dns_buf_t *buf)
{
    char c;
    c = buf->data[buf->size++];
    return c << 8 | buf->data[buf->size++];
}

/**
 * @brief dap_dns_buf_put_uint16 Put uint16 to network order
 * @param buf DNS buffer structure
 * @param val uint16 in host order
 * @return none
 */
void dap_dns_buf_put_uint16(dap_dns_buf_t *buf, uint16_t val)
{
    buf->data[buf->size++] = val >> 8;
    buf->data[buf->size++] = val;
}

/**
 * @brief dap_dns_buf_put_uint32 Put uint32 to network order
 * @param buf DNS buffer structure
 * @param val uint32 in host order
 * @return none
 */
void dap_dns_buf_put_uint32(dap_dns_buf_t *buf, uint32_t val)
{
    dap_dns_buf_put_uint16(buf, val >> 16);
    dap_dns_buf_put_uint16(buf, val);
}

/**
 * @brief dap_dns_buf_put_uint64 Put uint64 to network order
 * @param buf DNS buffer structure
 * @param val uint64 in host order
 * @return none
 */
void dap_dns_buf_put_uint64(dap_dns_buf_t *buf, uint64_t val)
{
    dap_dns_buf_put_uint32(buf, val >> 32);
    dap_dns_buf_put_uint32(buf, val);
}