/*
 * Creator: Naman Dixit
 * Notice: © Copyright 2020 Naman Dixit
 */

#include "nlib/nlib.h"

#undef internal_malloc
#undef internal_free

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <assert.h>
#include <signal.h>
#include <pthread.h>
#include <librdkafka/rdkafka.h>

# if defined(COMPILER_CLANG)
#  pragma clang diagnostic push
#   pragma clang diagnostic ignored "-Wpadded"
#   pragma clang diagnostic ignored "-Wfloat-equal"
# endif
#include "cJSON/cJSON.h"
#include "cJSON/cJSON.c"
# if defined(COMPILER_CLANG)
#  pragma clang diagnostic pop
# endif

# if defined(COMPILER_CLANG)
#  pragma clang diagnostic push
#   pragma clang diagnostic ignored "-Wreserved-id-macro"
#   pragma clang diagnostic ignored "-Wcast-qual"
# endif
#include "inih/ini.h"
#include "inih/ini.c"
# if defined(COMPILER_CLANG)
#  pragma clang diagnostic pop
# endif

typedef struct Resources {
    Sint memory;
} Resources;

typedef struct Command {
    Char *txn_id;
    Resources res;
} Command;

#include "kafka.h"
#include "time.c"
#include "conf.c"

#include "command.h"

typedef struct Thread_Manager_Command {
    enum Thread_Manager_Command_Kind {
        Thread_Manager_Command_NONE,
        Thread_Manager_Command_DOCKER_CREATE,
        Thread_Manager_Command_DOCKER_DESTROY,
    } kind;

    Char *entity_id;
    Char *resource_id;
} Thread_Manager_Command;

typedef struct JSON_Print_Command {
    Char *msg;
    Char *topic;
} JSON_Print_Command;

# if defined(COMPILER_CLANG)
#  pragma clang diagnostic push
#   pragma clang diagnostic ignored "-Wextra-semi"
# endif

COMMAND_SYSTEM(Thread_Manager_Request, tmCommand,  Thread_Manager_Command,
               JSON_Print_Request,    instrumentCommand, JSON_Print_Command);

# if defined(COMPILER_CLANG)
#  pragma clang diagnostic pop
# endif

global_variable volatile sig_atomic_t global_keep_running = 1;
global_variable Char *node_name;

#include "instrument_docker.c"
#include "thread_manager.c"

internal_function
void signalHandlerSIGINT (int _)
{
    (void)_;
    global_keep_running = 0;
}

int main(int argc, char** argv)
{
    if (argc > 1) {
        node_name = argv[1];
    } else {
        Char hostname[1024] = {0};
        gethostname(hostname, 1023);
        sbufPrint(node_name, "%s", hostname);
    }

    signal(SIGINT, signalHandlerSIGINT);

    { // Init third party libraries
        { // Init cJSON
            cJSON_Hooks hook = {.malloc_fn = dlmalloc,
                                .free_fn = dlfree};
            cJSON_InitHooks(&hook);
        }

        { // Init
            prom_collector_registry_default_init();
        }
    }

    Configuration conf = {0};
    { // Default config values
        conf.kafka_address = "10.129.6.5:9092";

        conf.message_read_gap = 10;
        conf.heartbeat_gap = 1000;
    }

    if (ini_parse("config.ini", confCallback, &conf) < 0) {
        printf("Can't load 'config.ini'\n");
        return -1;
    }

    tmCommandInit();
    instrumentCommandInit();

    pthread_t thread_manager;
    pthread_create(&thread_manager, NULL, &tmProcessLoop, NULL);

    Kafka kafka = {0};

    kafka.writer = kafkaCreateWriter(&kafka, conf.kafka_address);

#define CREATE_TOPIC(s)                                 \
    do {                                                \
        if (kafkaCreateTopic(&kafka, s, 1, 1) == -1) {  \
            rd_kafka_destroy(kafka.writer);             \
            return -1;                                  \
        }                                               \
    } while (0)

    CREATE_TOPIC("REQUEST_DM_2_RM"); //
    CREATE_TOPIC("RESPONSE_RM_2_DM");
    CREATE_TOPIC("REQUEST_RM_2_RD");
    CREATE_TOPIC("RESPONSE_RD_2_RM"); //
    CREATE_TOPIC("JOIN_RD_2_RM"); //
    CREATE_TOPIC("HEARTBEAT_RD_2_RM"); //
    CREATE_TOPIC("JOIN_ACK_RM_2_RD"); //
    CREATE_TOPIC("REJOIN_RM_2_RD"); //
    CREATE_TOPIC("LOG_COMMON"); //

    kafka.reader = kafkaCreateReader(&kafka, conf.kafka_address);

    rd_kafka_topic_partition_list_t *kafka_reader_topics = rd_kafka_topic_partition_list_new(1);

    rd_kafka_topic_t *topic_req_a2g = kafkaSubscribe(&kafka, kafka_reader_topics,
                                                     "REQUEST_RM_2_RD");
    rd_kafka_topic_t *topic_jac_a2g = kafkaSubscribe(&kafka, kafka_reader_topics,
                                                     "JOIN_ACK_RM_2_RD");
    rd_kafka_topic_t *topic_rej_a2g = kafkaSubscribe(&kafka, kafka_reader_topics,
                                                     "REJOIN_RM_2_RD");
    rd_kafka_topic_t *topic_log = kafkaSubscribe(&kafka, kafka_reader_topics,
                                                 "LOG_COMMON");

    rd_kafka_resp_err_t kafka_reader_topics_err = rd_kafka_subscribe(kafka.reader,
                                                                     kafka_reader_topics);

    printf("Subscription finished\n");
    fflush(stdout);

    rd_kafka_topic_partition_list_destroy(kafka_reader_topics);

    printf("Partition list destroyed\n");
    fflush(stdout);

    if (kafka_reader_topics_err) {
        fprintf(stderr, "Subscribe failed: %s\n",
                rd_kafka_err2str(kafka_reader_topics_err));
        rd_kafka_destroy(kafka.reader);
        return -1;
    }

    {
        Sint timestamp = (Sint)time(0);
        Char *join_msg = NULL;
        sbufPrint(join_msg, "{\"node_id\": \"%s\"", node_name);
        sbufPrint(join_msg, ",\n\"timestamp\": %d", timestamp);
        sbufPrint(join_msg, "\n}\n");

        if (!kafkaWrite(kafka.writer, "JOIN_RD_2_RM", "resource_daemon", join_msg)) {
            return -1;
        }
    }

    B32 join_successful = false;

    U64 time_begin = timeMilli();
    U64 time_accum = 0;

    Sint time_of_launch = (Sint)time(0);

    while (global_keep_running) {
        // NOTE(naman): Get the fd's that are ready
        rd_kafka_message_t *kafka_message_read = rd_kafka_consumer_poll(kafka.reader,
                                                                        conf.message_read_gap);

        B32 command_found = false;
        Command c = {0};

        if (kafka_message_read != NULL) {
            fprintf(stderr,
                    "Received message on %s [%d] "
                    "at offset %"PRId64": \n%s\n",
                    rd_kafka_topic_name(kafka_message_read->rkt),
                    (int)kafka_message_read->partition, kafka_message_read->offset,
                    cJSON_Print(cJSON_Parse((char *)kafka_message_read->payload)));

            char *buffer = (char *)kafka_message_read->payload;

            const Char *json_error = NULL;
            cJSON *root = cJSON_ParseWithOpts(buffer, &json_error, true);

            if ((cJSON_GetObjectItem(root, "timestamp") == NULL) ||
                (cJSON_GetObjectItem(root, "timestamp")->valueint) < time_of_launch) {
                printf("Ignoring : %s\n", buffer);
                cJSON_Delete(root);
                rd_kafka_message_destroy(kafka_message_read);
                continue;
            }

            if (kafka_message_read->err) {
                /* Consumer error: typically just informational. */
                fprintf(stderr, "Consumer error: %s\n",
                        rd_kafka_message_errstr(kafka_message_read));
            } else if (kafka_message_read->rkt == topic_jac_a2g) {
                join_successful = true;
            } else if (join_successful) {
                if (kafka_message_read->rkt == topic_req_a2g) {
                    if (root == NULL) {
                        // TODO(naman): Error
                    } else {
                        command_found = true;
                        c.txn_id = cJSON_GetObjectItem(root, "resource_id")->valuestring;
                        c.res.memory = cJSON_GetObjectItem(root, "memory")->valueint;
                    }
                } else if (kafka_message_read->rkt == topic_rej_a2g) {
                    Char *node_id = cJSON_GetObjectItem(root, "node_id")->valuestring;

                    if (stringEqual(node_id, node_name)) {
                        join_successful = false;

                        Sint timestamp = (Sint)time(0);
                        Char *rejoin_msg = NULL;
                        sbufPrint(rejoin_msg, "{\"node_id\": \"%s\"", node_name);
                        sbufPrint(rejoin_msg, ",\n\"timestamp\": %d", timestamp);
                        sbufPrint(rejoin_msg, "\n}\n");

                        if (!kafkaWrite(kafka.writer, "JOIN_RD_2_RM", "resource_daemon", rejoin_msg)) {
                            return -1;
                        }
                    }
                } else if (kafka_message_read->rkt == topic_log) {
                    cJSON *msg_type_json = cJSON_GetObjectItem(root, "message_type");
                    if (msg_type_json == NULL) {
                        if (stringEqual(msg_type_json->valuestring, "deployment_launch")) {
                            Char *node_id = cJSON_GetObjectItem(root, "node_id")->valuestring;
                            if (stringEqual(node_id, node_name)) {
                                Char *resource_id = cJSON_GetObjectItem(root, "resource_id")->valuestring;
                                Char *entity_id = cJSON_GetObjectItem(root, "entity_id")->valuestring;
                                Char *entity_type = cJSON_GetObjectItem(root, "entity_type")->valuestring;

                                Thread_Manager_Command tmc = {.entity_id = strdup(entity_id),
                                                              .resource_id = strdup(resource_id)};
                                B32 add_command = false;

                                if (stringEqual(entity_type, "docker")) {
                                    tmc.kind = Thread_Manager_Command_DOCKER_CREATE;
                                    add_command = true;
                                }

                                if (add_command) {
                                    tmCommandEnqueue(tmc);
                                } else {
                                    free(tmc.entity_id);
                                    free(tmc.resource_id);
                                }
                            }
                        }
                    }
                }
            }
            rd_kafka_message_destroy(kafka_message_read);
        }

        if (join_successful) {
            int memory = 0;

            FILE *meminfo = fopen("/proc/meminfo", "r");
            Char line[256] = {0};
            while(fgets(line, sizeof(line), meminfo)) {
                if (sscanf(line, "MemAvailable: %d kB", &memory) == 1) {
                    fclose(meminfo);
                    break;
                }
            }
            memory /= 1024;

            if (command_found) {
                Char *output = NULL;

                Sint timestamp = (Sint)time(0);

                sbufPrint(output, "{\n\"node_id\": \"%s\"", node_name);
                sbufPrint(output, ",\n\"resource_id\": \"%s\"", c.txn_id);
                sbufPrint(output, ",\n\"timestamp\": %d", timestamp);

                if (memory >= c.res.memory) {
                    sbufPrint(output, ",\n\"success\": true\n");
                    // TODO(naman): Add port
                    // sbufPrint(output, ",\n\"port\": %d", port);
                } else {
                    sbufPrint(output, ",\n\"success\": false\n");
                }

                sbufPrint(output, "\n}\n");

                if (!kafkaWrite(kafka.writer, "RESPONSE_RD_2_RM", "resource_daemon", output)) {
                    return -1;
                }
            }

            {
                JSON_Print_Command command = {0};
                while (instrumentCommandDequeue(&command)) {
                    // TODO(naman): Enable this after proper testing
                    if (!kafkaWrite(kafka.writer, command.topic, "resource_daemon", command.msg)) {
                        return -1;
                    }
                }
            }

            { // Send a heartbeat message if it is time to do so
                U64 time_new = timeMilli();
                U64 time_passed = time_new - time_begin;
                time_begin = time_new;
                time_accum += time_passed;

                if (time_accum >= conf.heartbeat_gap) {
                    time_accum = 0;

                    Char *output = NULL;
                    Sint timestamp = (Sint)time(0);

                    sbufPrint(output, "{\"node_id\": \"%s\"", node_name);
                    sbufPrint(output, ",\n\"timestamp\": %d", timestamp);
                    sbufPrint(output, ",\n\"memory\": %d", memory);

                    sbufPrint(output, "\n}\n");

                    if (!kafkaWrite(kafka.writer, "HEARTBEAT_RD_2_RM", "resource_daemon", output)) {
                        return -1;
                    }
                }
            }
        }
    }

    for (Size i = 0; i < sbufElemin(kafka.topics); i++) {
        rd_kafka_topic_destroy(kafka.topics[i]);
    }
    rd_kafka_consumer_close(kafka.reader);
    rd_kafka_destroy(kafka.reader);
    for (Size i = 0; i < sbufElemin(kafka.queues); i++) {
        rd_kafka_queue_destroy(kafka.queues[i]);
    }
    rd_kafka_destroy(kafka.writer);

    return 0;
}
