#include <dbus-1.0/dbus/dbus.h>
#include <signal.h>
#include <stdint.h>
#include <syslog.h>

#include "nDPIsrvd.h"
#include "utstring.h"
#include "utils.h"

#define SLEEP_TIME_IN_S (3)

struct flow_user_data
{
    nDPIsrvd_ull detected_risks;
};

enum dbus_level
{
    DBUS_LOW = 0,
    DBUS_NORMAL,
    DBUS_CRITICAL
};

static char const * const flow_severities[] = {"Low", "Medium", "High", "Severe", "Critical", "Emergency"};
static char const * const flow_breeds[] = {
    "Safe", "Acceptable", "Fun", "Unsafe", "Potentially Dangerous", "Tracker\\/Ads", "Dangerous", "Unrated", "???"};
static char const * const flow_categories[] = {"Unspecified",
                                               "Media",
                                               "VPN",
                                               "Email",
                                               "DataTransfer",
                                               "Web",
                                               "SocialNetwork",
                                               "Download",
                                               "Game",
                                               "Chat",
                                               "VoIP",
                                               "Database",
                                               "RemoteAccess",
                                               "Cloud",
                                               "Network",
                                               "Collaborative",
                                               "RPC",
                                               "Streaming",
                                               "System",
                                               "SoftwareUpdate",
                                               "Music",
                                               "Video",
                                               "Shopping",
                                               "Productivity",
                                               "FileSharing",
                                               "ConnCheck",
                                               "IoT-Scada",
                                               "VirtAssistant",
                                               "Cybersecurity",
                                               "AdultContent",
                                               "Mining",
                                               "Malware",
                                               "Advertisement",
                                               "Banned_Site",
                                               "Site_Unavailable",
                                               "Allowed_Site",
                                               "Antimalware",
                                               "Crypto_Currency"};

static uint8_t desired_flow_severities[nDPIsrvd_ARRAY_LENGTH(flow_severities)] = {};
static uint8_t desired_flow_breeds[nDPIsrvd_ARRAY_LENGTH(flow_breeds)] = {};
static uint8_t desired_flow_categories[nDPIsrvd_ARRAY_LENGTH(flow_categories)] = {};

static unsigned int id = 0;
static char const * const application = "nDPIsrvd.notifyd";

static int main_thread_shutdown = 0;

static char * pidfile = NULL;
static char * serv_optarg = NULL;

#ifdef ENABLE_MEMORY_PROFILING
void nDPIsrvd_memprof_log_alloc(size_t alloc_size)
{
    (void)alloc_size;
}

void nDPIsrvd_memprof_log_free(size_t free_size)
{
    (void)free_size;
}

void nDPIsrvd_memprof_log(char const * const format, ...)
{
    va_list ap;

    va_start(ap, format);
    fprintf(stderr, "%s", "nDPIsrvd MemoryProfiler: ");
    vfprintf(stderr, format, ap);
    fprintf(stderr, "%s\n", "");
    va_end(ap);
}
#endif

static void send_to_dbus(char const * const icon,
                         char const * const urgency,
                         enum dbus_level level,
                         char const * const summary,
                         char const * const body,
                         int timeout)
{
    DBusConnection * connection = dbus_bus_get(DBUS_BUS_SESSION, 0);
    DBusMessage * message = dbus_message_new_method_call("org.freedesktop.Notifications",
                                                         "/org/freedesktop/Notifications",
                                                         "org.freedesktop.Notifications",
                                                         "Notify");
    DBusMessageIter iter[4];
    dbus_message_iter_init_append(message, iter);
    dbus_message_iter_append_basic(iter, 's', &application);
    dbus_message_iter_append_basic(iter, 'u', &id);
    dbus_message_iter_append_basic(iter, 's', &icon);
    dbus_message_iter_append_basic(iter, 's', &summary);
    dbus_message_iter_append_basic(iter, 's', &body);
    dbus_message_iter_open_container(iter, 'a', "s", iter + 1);
    dbus_message_iter_close_container(iter, iter + 1);
    dbus_message_iter_open_container(iter, 'a', "{sv}", iter + 1);
    dbus_message_iter_open_container(iter + 1, 'e', 0, iter + 2);
    dbus_message_iter_append_basic(iter + 2, 's', &urgency);
    dbus_message_iter_open_container(iter + 2, 'v', "y", iter + 3);
    dbus_message_iter_append_basic(iter + 3, 'y', &level);
    dbus_message_iter_close_container(iter + 2, iter + 3);
    dbus_message_iter_close_container(iter + 1, iter + 2);
    dbus_message_iter_close_container(iter, iter + 1);
    dbus_message_iter_append_basic(iter, 'i', &timeout);
    dbus_connection_send(connection, message, 0);
    dbus_connection_flush(connection);
    dbus_message_unref(message);
    dbus_connection_unref(connection);

    id++;
}

static void notify(enum dbus_level level, char const * const summary, int timeout, char const * const body)
{
    send_to_dbus("dialog-information", "urgency", level, summary, body, timeout);
}

__attribute__((format(printf, 4, 5))) static void notifyf(
    enum dbus_level level, char const * const summary, int timeout, char const * const body_fmt, ...)
{
    va_list ap;
    char buf[BUFSIZ];

    va_start(ap, body_fmt);
    if (vsnprintf(buf, sizeof(buf), body_fmt, ap) > 0)
    {
        notify(level, summary, timeout, buf);
    }
    va_end(ap);
}

static ssize_t get_value_index(char const * const possible_values[],
                               size_t possible_values_size,
                               char const * const needle,
                               size_t needle_len)
{
    size_t i;

    for (i = 0; i < possible_values_size; ++i)
    {
        if (strncmp(needle, possible_values[i], needle_len) == 0)
        {
            break;
        }
    }

    if (i == possible_values_size)
    {
        return -1;
    }

    return i;
}

static void check_value(char const * const possible_values[],
                        size_t possible_values_size,
                        char const * const needle,
                        size_t needle_len)
{
    if (get_value_index(possible_values, possible_values_size, needle, needle_len) == -1)
    {
        syslog(LOG_DAEMON | LOG_ERR, "BUG: Unknown value: %.*s", (int)needle_len, needle);
        notifyf(DBUS_CRITICAL, "BUG", 5000, "Unknown value: %.*s", (int)needle_len, needle);
    }
}

static enum nDPIsrvd_callback_return notifyd_json_callback(struct nDPIsrvd_socket * const sock,
                                                           struct nDPIsrvd_instance * const instance,
                                                           struct nDPIsrvd_thread_data * const thread_data,
                                                           struct nDPIsrvd_flow * const flow)
{
    (void)instance;
    (void)thread_data;

    struct nDPIsrvd_json_token const * const flow_event_name = TOKEN_GET_SZ(sock, "flow_event_name");
    struct flow_user_data * flow_user_data = NULL;

    if (flow != NULL)
    {
        flow_user_data = (struct flow_user_data *)flow->flow_user_data;
    }

    if (TOKEN_VALUE_EQUALS_SZ(sock, flow_event_name, "detected") != 0 ||
        TOKEN_VALUE_EQUALS_SZ(sock, flow_event_name, "detection-update") != 0 ||
        TOKEN_VALUE_EQUALS_SZ(sock, flow_event_name, "update") != 0)
    {
        struct nDPIsrvd_json_token const * const flow_risks = TOKEN_GET_SZ(sock, "ndpi", "flow_risk");
        struct nDPIsrvd_json_token const * current = NULL;
        int next_child_index = -1, desired_severity_found = 0;
        UT_string risks;

        utstring_init(&risks);

        if (flow_risks != NULL)
        {
            while ((current = nDPIsrvd_get_next_token(sock, flow_risks, &next_child_index)) != NULL)
            {
                nDPIsrvd_ull numeric_risk_value = (nDPIsrvd_ull)-1;
                size_t flow_risk_key_len = 0;
                char const * const flow_risk_key = TOKEN_GET_KEY(sock, current, &flow_risk_key_len);

                if (flow_risk_key == NULL || flow_risk_key_len == 0)
                {
                    continue;
                }

                if (str_value_to_ull(flow_risk_key, &numeric_risk_value) == CONVERSION_OK && flow_user_data != NULL &&
                    (flow_user_data->detected_risks & (1ull << numeric_risk_value)) == 0)
                {
                    flow_user_data->detected_risks |= (1ull << (numeric_risk_value - 1));

                    char flow_risk_sz[flow_risk_key_len + 1];
                    snprintf(flow_risk_sz, sizeof(flow_risk_sz), "%llu", numeric_risk_value);
                    size_t flow_risk_len = 0;
                    size_t flow_severity_len = 0;
                    char const * const flow_risk_str =
                        TOKEN_GET_VALUE(sock,
                                        TOKEN_GET_SZ(sock, "ndpi", "flow_risk", flow_risk_sz, "risk"),
                                        &flow_risk_len);
                    char const * const flow_severity_str =
                        TOKEN_GET_VALUE(sock,
                                        TOKEN_GET_SZ(sock, "ndpi", "flow_risk", flow_risk_sz, "severity"),
                                        &flow_severity_len);

                    if (flow_risk_str == NULL || flow_risk_len == 0 || flow_severity_str == NULL ||
                        flow_severity_len == 0)
                    {
                        continue;
                    }

                    ssize_t severity_index = get_value_index(flow_severities,
                                                             nDPIsrvd_ARRAY_LENGTH(flow_severities),
                                                             flow_severity_str,
                                                             flow_severity_len);
                    if (severity_index != -1 && desired_flow_severities[severity_index] != 0)
                    {
                        desired_severity_found = 1;
                    }
                    utstring_printf(&risks,
                                    "Risk: '%.*s'\n"
                                    "Severity: '%.*s'\n",
                                    (int)flow_risk_len,
                                    flow_risk_str,
                                    (int)flow_severity_len,
                                    flow_severity_str);
                    check_value(flow_severities,
                                nDPIsrvd_ARRAY_LENGTH(flow_severities),
                                flow_severity_str,
                                flow_severity_len);
                }
            }
        }

        {
            size_t flow_srcip_len = 0;
            size_t flow_dstip_len = 0;
            size_t flow_breed_len = 0;
            size_t flow_category_len = 0;
            size_t flow_hostname_len = 0;

            char const * const flow_srcip = TOKEN_GET_VALUE(sock, TOKEN_GET_SZ(sock, "src_ip"), &flow_srcip_len);
            char const * const flow_dstip = TOKEN_GET_VALUE(sock, TOKEN_GET_SZ(sock, "dst_ip"), &flow_dstip_len);
            char const * const flow_breed_str =
                TOKEN_GET_VALUE(sock, TOKEN_GET_SZ(sock, "ndpi", "breed"), &flow_breed_len);
            char const * const flow_category_str =
                TOKEN_GET_VALUE(sock, TOKEN_GET_SZ(sock, "ndpi", "category"), &flow_category_len);
            char const * const flow_hostname =
                TOKEN_GET_VALUE(sock, TOKEN_GET_SZ(sock, "ndpi", "hostname"), &flow_hostname_len);

            if (flow_breed_str != NULL && flow_breed_len != 0 && flow_category_str != NULL && flow_category_len != 0)
            {
                ssize_t breed_index =
                    get_value_index(flow_breeds, nDPIsrvd_ARRAY_LENGTH(flow_breeds), flow_breed_str, flow_breed_len);
                ssize_t category_index = get_value_index(flow_categories,
                                                         nDPIsrvd_ARRAY_LENGTH(flow_categories),
                                                         flow_category_str,
                                                         flow_category_len);

                if ((breed_index != -1 && desired_flow_breeds[breed_index] != 0) ||
                    (category_index != -1 && desired_flow_categories[category_index] != 0) ||
                    desired_severity_found != 0)
                {
                    notifyf(DBUS_CRITICAL,
                            "Flow Notification",
                            5000,
                            "%.*s -> %.*s (%.*s)\nBreed: '%.*s', Category: '%.*s'\n%s",
                            (int)flow_srcip_len,
                            flow_srcip,
                            (int)flow_dstip_len,
                            flow_dstip,
                            (flow_hostname_len > 0 ? (int)flow_hostname_len : 1),
                            (flow_hostname_len > 0 ? flow_hostname : "-"),
                            (int)flow_breed_len,
                            flow_breed_str,
                            (int)flow_category_len,
                            flow_category_str,
                            (utstring_len(&risks) > 0 ? utstring_body(&risks) : "No flow risks detected\n"));
                }

                check_value(flow_breeds, nDPIsrvd_ARRAY_LENGTH(flow_breeds), flow_breed_str, flow_breed_len);
                check_value(flow_categories,
                            nDPIsrvd_ARRAY_LENGTH(flow_categories),
                            flow_category_str,
                            flow_category_len);
            }
            else if (desired_severity_found != 0)
            {
                notifyf(DBUS_CRITICAL,
                        "Risky Flow",
                        5000,
                        "%.*s -> %.*s (%.*s)\n%s",
                        (int)flow_srcip_len,
                        flow_srcip,
                        (int)flow_dstip_len,
                        flow_dstip,
                        (flow_hostname_len > 0 ? (int)flow_hostname_len : 1),
                        (flow_hostname_len > 0 ? flow_hostname : "-"),
                        utstring_body(&risks));
            }
        }

        utstring_done(&risks);
    }

    return CALLBACK_OK;
}

static void print_usage(char const * const arg0)
{
    static char const usage[] =
        "Usage: %s "
        "[-s host] [-C category...] [-B breed...] [-S severity...]\n\n"
        "\t-s\tDestination where nDPIsrvd is listening on.\n"
        "\t-C\tDesired nDPI category which fires a notificiation.\n"
        "\t  \tCan be specified multiple times.\n"
        "\t-B\tDesired nDPI breed which fires a notification.\n"
        "\t  \tCan be specified multiple times.\n"
        "\t-S\tDesired nDPI risk severity which fires a notification.\n"
        "\t  \tCan be specified multiple times.\n"
        "\n"
        "Possible values for `-C': %s\n"
        "Possible values for `-B': %s\n"
        "Possible values for `-S': %s\n"
        "\n";

    UT_string flow_categories_str, flow_breeds_str, flow_severities_str;
    utstring_init(&flow_categories_str);
    utstring_init(&flow_breeds_str);
    utstring_init(&flow_severities_str);
    for (size_t i = 0; i < nDPIsrvd_ARRAY_LENGTH(flow_categories); ++i)
    {
        utstring_printf(&flow_categories_str, "%s, ", flow_categories[i]);
    }
    flow_categories_str.d[flow_categories_str.i - 2] = '\0';
    for (size_t i = 0; i < nDPIsrvd_ARRAY_LENGTH(flow_breeds); ++i)
    {
        utstring_printf(&flow_breeds_str, "%s, ", flow_breeds[i]);
    }
    flow_breeds_str.d[flow_breeds_str.i - 2] = '\0';
    for (size_t i = 0; i < nDPIsrvd_ARRAY_LENGTH(flow_severities); ++i)
    {
        utstring_printf(&flow_severities_str, "%s, ", flow_severities[i]);
    }
    flow_severities_str.d[flow_severities_str.i - 2] = '\0';
    fprintf(stderr,
            usage,
            arg0,
            utstring_body(&flow_categories_str),
            utstring_body(&flow_breeds_str),
            utstring_body(&flow_severities_str));
    utstring_done(&flow_severities_str);
    utstring_done(&flow_breeds_str);
    utstring_done(&flow_categories_str);
}

static int set_defaults(void)
{
    char const * const default_severities[] = {"High", "Severe", "Critical", "Emergency"};
    char const * const default_breeds[] = {"Unsafe", "Potentially Dangerous", "Dangerous", "Unrated"};
    char const * const default_categories[] = {"Mining", "Malware", "Banned_Site", "Crypto_Currency"};

    for (size_t i = 0; i < nDPIsrvd_ARRAY_LENGTH(default_severities); ++i)
    {
        ssize_t index = get_value_index(flow_severities,
                                        nDPIsrvd_ARRAY_LENGTH(flow_severities),
                                        default_severities[i],
                                        strlen(default_severities[i]));
        if (index == -1)
        {
            return 1;
        }
        desired_flow_severities[index] = 1;
    }

    for (size_t i = 0; i < nDPIsrvd_ARRAY_LENGTH(default_breeds); ++i)
    {
        ssize_t index = get_value_index(flow_breeds,
                                        nDPIsrvd_ARRAY_LENGTH(flow_breeds),
                                        default_breeds[i],
                                        strlen(default_breeds[i]));
        if (index == -1)
        {
            return 1;
        }
        desired_flow_breeds[index] = 1;
    }

    for (size_t i = 0; i < nDPIsrvd_ARRAY_LENGTH(default_categories); ++i)
    {
        ssize_t index = get_value_index(flow_categories,
                                        nDPIsrvd_ARRAY_LENGTH(flow_categories),
                                        default_categories[i],
                                        strlen(default_categories[i]));
        if (index == -1)
        {
            return 1;
        }
        desired_flow_categories[index] = 1;
    }

    return 0;
}

static int parse_options(int argc, char ** argv, struct nDPIsrvd_socket * const sock)
{
    int opt, force_defaults = 1;

    while ((opt = getopt(argc, argv, "hdp:s:C:B:S:")) != -1)
    {
        switch (opt)
        {
            case 'd':
                daemonize_enable();
                break;
            case 'p':
                free(pidfile);
                pidfile = strdup(optarg);
                break;
            case 's':
                free(serv_optarg);
                serv_optarg = strdup(optarg);
                break;
            case 'C':
            {
                ssize_t index =
                    get_value_index(flow_categories, nDPIsrvd_ARRAY_LENGTH(flow_categories), optarg, strlen(optarg));
                if (index == -1)
                {
                    fprintf(stderr, "Invalid argument for `-C': %s\n", optarg);
                    return 1;
                }
                else
                {
                    desired_flow_categories[index] = 1;
                }
                force_defaults = 0;
                break;
            }
            case 'B':
            {
                ssize_t index =
                    get_value_index(flow_breeds, nDPIsrvd_ARRAY_LENGTH(flow_breeds), optarg, strlen(optarg));
                if (index == -1)
                {
                    fprintf(stderr, "Invalid argument for `-B': %s\n", optarg);
                    return 1;
                }
                else
                {
                    desired_flow_breeds[index] = 1;
                }
                force_defaults = 0;
                break;
            }
            case 'S':
            {
                ssize_t index =
                    get_value_index(flow_severities, nDPIsrvd_ARRAY_LENGTH(flow_severities), optarg, strlen(optarg));
                if (index == -1)
                {
                    fprintf(stderr, "Invalid argument for `-S': %s\n", optarg);
                    return 1;
                }
                else
                {
                    desired_flow_severities[index] = 1;
                }
                force_defaults = 0;
                break;
            }
            default:
                print_usage(argv[0]);
                return 1;
        }
    }

    if (force_defaults != 0 && set_defaults() != 0)
    {
        fprintf(stderr, "%s\n", "BUG: Could not set default values.");
        syslog(LOG_DAEMON | LOG_ERR, "%s\n", "BUG: Could not set default values.");
        return 1;
    }

    if (serv_optarg == NULL)
    {
        serv_optarg = strdup(DISTRIBUTOR_UNIX_SOCKET);
    }

    if (nDPIsrvd_setup_address(&sock->address, serv_optarg) != 0)
    {
        syslog(LOG_DAEMON | LOG_ERR, "Could not parse address `%s'", serv_optarg);
        return 1;
    }

    if (optind < argc)
    {
        syslog(LOG_DAEMON | LOG_ERR, "%s", "Unexpected argument after options");
        return 1;
    }

    return 0;
}

static void sighandler(int signum)
{
    switch (signum)
    {
        case SIGINT:
            notify(DBUS_LOW, "nDPIsrvd-notifyd", 3000, "Received SIGINT, shutdown.");
            break;
        case SIGTERM:
            notify(DBUS_LOW, "nDPIsrvd-notifyd", 3000, "Received SIGTERM, shutdown.");
            break;
        default:
            notify(DBUS_LOW, "nDPIsrvd-notifyd", 3000, "Received unknown signal, shutdown.");
            break;
    }

    main_thread_shutdown++;
}

int main(int argc, char ** argv)
{
    signal(SIGINT, sighandler);
    signal(SIGTERM, sighandler);
    signal(SIGPIPE, SIG_IGN);

    openlog("nDPIsrvd-notifyd", LOG_CONS, LOG_DAEMON);

    struct nDPIsrvd_socket * sock =
        nDPIsrvd_socket_init(0, 0, 0, sizeof(struct flow_user_data), notifyd_json_callback, NULL, NULL);
    if (sock == NULL)
    {
        syslog(LOG_DAEMON | LOG_ERR, "%s", "nDPIsrvd socket memory allocation failed!");
        return 1;
    }

    if (parse_options(argc, argv, sock) != 0)
    {
        goto failure;
    }

    if (daemonize_with_pidfile(pidfile) != 0)
    {
        return 1;
    }

    int previous_connect_succeeded = 1;
    do
    {
        if (nDPIsrvd_connect(sock) != CONNECT_OK)
        {
            if (main_thread_shutdown != 0)
            {
                break;
            }
            if (previous_connect_succeeded != 0)
            {
                notifyf(DBUS_CRITICAL, "nDPIsrvd-notifyd", 3000, "nDPIsrvd socket connect to %s failed!", serv_optarg);
                syslog(LOG_DAEMON | LOG_ERR, "nDPIsrvd socket connect to %s failed!", serv_optarg);
                previous_connect_succeeded = 0;
            }
            nDPIsrvd_socket_close(sock);
            sleep(SLEEP_TIME_IN_S);
            continue;
        }
        previous_connect_succeeded = 1;

        if (nDPIsrvd_set_read_timeout(sock, 3, 0) != 0)
        {
            syslog(LOG_DAEMON | LOG_ERR, "nDPIsrvd set read timeout failed: %s", strerror(errno));
            goto failure;
        }

        notifyf(DBUS_NORMAL, "nDPIsrvd-notifyd", 3000, "Connected to '%s'", serv_optarg);
        syslog(LOG_DAEMON | LOG_NOTICE, "%s", "Initialization succeeded.");

        while (main_thread_shutdown == 0)
        {
            enum nDPIsrvd_read_return read_ret = nDPIsrvd_read(sock);
            if (errno == EINTR)
            {
                continue;
            }
            if (read_ret == READ_TIMEOUT)
            {
                continue;
            }
            if (read_ret != READ_OK)
            {
                notifyf(DBUS_CRITICAL, "nDPIsrvd-notifyd", 3000, "nDPIsrvd socket read from %s failed!", serv_optarg);
                syslog(LOG_DAEMON | LOG_ERR, "nDPIsrvd socket read from %s failed!", serv_optarg);
                break;
            }

            enum nDPIsrvd_parse_return parse_ret = nDPIsrvd_parse_all(sock);
            if (parse_ret != PARSE_NEED_MORE_DATA)
            {
                syslog(LOG_DAEMON | LOG_ERR,
                       "Could not parse JSON message %s: %.*s\n",
                       nDPIsrvd_enum_to_string(parse_ret),
                       nDPIsrvd_json_buffer_length(sock),
                       nDPIsrvd_json_buffer_string(sock));
                break;
            }
        }

        nDPIsrvd_socket_close(sock);
        notifyf(DBUS_NORMAL, "nDPIsrvd-notifyd", 3000, "Disconnected from '%s'.", serv_optarg);
        if (main_thread_shutdown == 0)
        {
            sleep(SLEEP_TIME_IN_S);
        }
    } while (main_thread_shutdown == 0);

failure:
    nDPIsrvd_socket_free(&sock);
    daemonize_shutdown(pidfile);
    closelog();

    return 0;
}
