#include <libconfig.h>
#include <dirent.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdarg.h>
#include <sys/stat.h>
#include "common.h"


#define print_message(sink, format, file, line) fprintf(stderr, format, file, line)                                                                                                                                               

typedef struct FileList FileList;

struct FileList {
    char *name;
    struct FileList *prev;
};

static void free_file_list(FileList *fl) {
    while (fl != NULL) {
        FileList *prev = fl->prev;
        free(fl->name);
        free(fl);
        fl = prev;
    }
}

static FILE *err_log;
static bool systemd_invoked = false;
static const char
        *socket_ext = ".socket",
        *dropin_ext = ".conf",
        *service_ext = ".service.d/",
        *fork_unit_file = "/sslh@",
        *select_unit_file = "/sslh-select@",
        *fork_unit = "# Automatically generated by systemd-sslh-generator\n\n"
                     "[Unit]\n"
                     "Requires=sslh@%s.socket\n"
                     "Conflicts=sslh-select@%s.service\n"
                     "PartOf=sslh@%s.socket\n",
        *select_unit = "# Automatically generated by systemd-sslh-generator\n\n"
                       "[Unit]\n"
                       "Requires=sslh@%s.socket\n"
                       "Conflicts=sslh@%s.service\n"
                       "PartOf=sslh@%s.socket\n";

static char *resolve_listen(const char *hostname, const char *port) {
    char *conn = calloc(1, strlen(hostname) + strlen(port) + 2);
    CHECK_ALLOC(conn, "malloc")
    strcpy(conn, hostname);
    strcat(conn, ":");
    strcat(conn, port);
    return conn;
}

static int get_listen_from_conf(const char *filename, char **listen[]) {
    config_t config;
    config_setting_t *setting, *addr;
    const char *hostname, *port;
    int len = 0;

    config_init(&config);
    if (config_read_file(&config, filename) == CONFIG_FALSE) {
        if (config_error_line(&config) != 0) {
            fprintf(err_log,
                    "systemd-sslh-generator: %s%d%s\n",
                    filename,
                    config_error_line(&config),
                    config_error_text(&config));
            return -1;
        }
    } else {
        setting = config_lookup(&config, "listen");
        if (setting) {
            len = config_setting_length(setting);
            *listen = malloc(len * sizeof(**listen));
            CHECK_ALLOC(*listen, "malloc")
            for (int i = 0; i < len; i++) {
                addr = config_setting_get_elem(setting, i);
                if (!(config_setting_lookup_string(addr, "host", &hostname) && config_setting_lookup_string(addr, "port", &port))) {
                    fprintf(err_log,
                            "systemd-sslh-generator: line %d:Incomplete specification (hostname and port required)\n",
                            config_setting_source_line(addr));
                    return -1;
                } else {
                    (*listen)[i] = resolve_listen(hostname, port);
                }
            }
        }
    }
    return len;
}

static void write_socket_unit(FILE *socket, char *listen[], int num_addr, const char *cfg, const char *source) {
    fprintf(socket,
            "# Automatically generated by systemd-sslh-generator\n\n"
            "[Unit]\n"
            "Before=sslh@%s.service\n"
            "SourcePath=%s\n"
            "PartOf=sslh@%s.service\n"
            "Documentation=man:sslh(8) man:systemd-sslh-generator(8)\n\n"
            "[Socket]\n"
            "FreeBind=true\n",
            cfg,
            source,
            cfg);
    for (int i = 0; i < num_addr; i++) {
        fprintf(socket, "ListenStream=%s\n", listen[i]);
    }
    fprintf(socket,
           "\n[Install]\n"
           "WantedBy=sockets.target\n");
}

static int write_unit_dropin(const char *runtime_unit_dir, const bool is_fork, const char *cfg) {
    const char
        *unit_file = is_fork ? fork_unit_file : select_unit_file,
        *unit_string = is_fork ? fork_unit : select_unit;
    FILE *dropin_fd = stdout;
    if (systemd_invoked) {
        //Systemd drop-in configuration for the base select service sslh@%I.service
        const size_t runtime_len = strlen(runtime_unit_dir);
        size_t len = strlen(unit_file) + strlen(cfg) + strlen(service_ext) + strlen(dropin_ext);
        char dropin_dir[runtime_len + len + 1];
        strcpy(dropin_dir, runtime_unit_dir);
        strcat(dropin_dir, unit_file);
        strcat(dropin_dir, cfg);
        strcat(dropin_dir, service_ext);

        if (mkdir(dropin_dir, S_IRWXU | S_IRWXG | S_IROTH)) {
            fprintf(err_log,
                    "systemd-sslh-generator: Could not create directory '%s' to generate drop-in configuration: %s\n",
                    dropin_dir,
                    strerror(errno));
            return errno;
        }

        len = len + strlen(cfg) + strlen(dropin_ext);
        char dropin_path[len + 1];
        strcpy(dropin_path, dropin_dir);
        strcat(dropin_path, cfg);
        strcat(dropin_path, dropin_ext);
        dropin_fd = fopen(dropin_path, "w");
        if (!dropin_fd) {
            fprintf(err_log,
                    "systemd-sslh-generator: Could not open '%s' to generate drop-in configuration: %s\n",
                    dropin_path,
                    strerror(errno));
            return errno;
        }
    }
    fprintf(dropin_fd, unit_string, cfg, cfg, cfg);
    if (systemd_invoked) {
        fclose(dropin_fd);
    }
    return 0;
}

static int gen_sslh_config(char *runtime_unit_dir) {
    int status = 0;
    const char *config_dir = "/etc/sslh/";
    char **listen;
    DIR *d = opendir(config_dir);
    FileList *fa = NULL;

    if (d) {
        struct dirent *dir;
        while ((dir = readdir(d)) != NULL) {
            if ((strcmp(dir->d_name, ".") == 0) || (strcmp(dir->d_name, "..") == 0)) {
                continue;
            }
            FileList *lfa = malloc(sizeof(FileList));
            CHECK_ALLOC(lfa, "malloc")
            lfa->name = malloc(strlen(dir->d_name) + 1);
            CHECK_ALLOC(lfa->name, "malloc")
            strcpy(lfa->name, dir->d_name);
            lfa->prev = NULL;
            if (fa) {
                lfa->prev = fa;
            }
            fa = lfa;
        }
        closedir(d);
    } else {
        fprintf(err_log,
                "systemd-sslh-generator: Configuration directory '/etc/sslh/' does not exist! No units generated.\n");
        //Config directory /etc/sslh/ does not exist
        return 0;
    }
    if (!fa) {
        fprintf(err_log,
                "systemd-sslh-generator: Configuration directory '/etc/sslh/' is empty! No units generated.\n");
        //No configuration files in /etc/sslh/
        return 0;
    }
    FileList *fa_ref = fa;
//    size_t num_listen_addresses = 0;
//    char **listen_addresses = NULL;
    //Process all config files
    do {
        char *end = strstr(fa->name, ".cfg");
        if (!end) {
            continue;
        }
        //Current sslh config name
        const size_t end_count = end - fa->name;
        char config_name[end_count + 1];
        memcpy(config_name, fa->name, end_count);
        config_name[end_count] = '\0';

        //Full path to current sslh config
        char full_path[strlen(config_dir) + strlen(fa->name) + 1];
        strcpy(full_path, config_dir);
        strcat(full_path, fa->name);
        FILE *config = fopen(full_path, "r");
        if (!config) {
            fprintf(err_log,
                    "systemd-sslh-generator: Could not open config file '%s': %s\n",
                    full_path,
                    strerror(errno));
            return errno;
        } else {
            fclose(config);
            int num_addr = get_listen_from_conf(full_path, &listen);
            if (num_addr <= 0) {
                fprintf(err_log,
                        "systemd-sslh-generator: sslh config '%s' contains no valid listen configurations!\n",
                        fa->name);
                status |= -1;
                continue;
            }
            FILE *socket_fd = stdout;
            if (systemd_invoked) {
                //Systemd socket for the current sslh config
                const size_t len = strlen(runtime_unit_dir) + strlen(fork_unit_file) + strlen(config_name) + strlen(socket_ext);
                char socket_path[len + 1];
                strcpy(socket_path, runtime_unit_dir);
                strcat(socket_path, fork_unit_file);
                strcat(socket_path, config_name);
                strcat(socket_path, socket_ext);
                socket_fd = fopen(socket_path, "w");
                if (!socket_fd) {
                    fprintf(err_log,
                            "systemd-sslh-generator: Could not open '%s' to generate socket configuration: %s\n",
                            socket_path,
                            strerror(errno));
                    status |= errno;
                    continue;
                }
            }
            //Write socket unit
            write_socket_unit(socket_fd, listen, num_addr, config_name, full_path);
            //Write forking drop-in config
            status |= write_unit_dropin(runtime_unit_dir, false, config_name);
            //Write select drop-in config
            status |= write_unit_dropin(runtime_unit_dir, true, config_name);
            if (systemd_invoked) {
                fclose(socket_fd);
            }
//            if (listen_addresses) {
//                //Check for overlapping addresses
//                for (size_t i = 0; i < num_listen_addresses; i++) {
//                    for (size_t j = 0; j < num_addr; j++) {
//                        if (strcmp(*(listen_addresses + i), *(listen + j)) == 0) {
//                            fprintf(err_log, "systemd-sslh-generator: Overlapping listen addresses across sslh configurations!");
//                            return -1;
//                        }
//                    }
//                }
//            }
//            char *listen_addresses_copy[num_listen_addresses + num_addr];
//            if (listen_addresses) {
//                //Copy previous configurations' listen addresses into temp
//                memcpy(listen_addresses_copy, listen_addresses, num_listen_addresses);
//                //Free global list of listen addresses
//                free(listen_addresses);
//            }
//            //Copy listen addresses from current configuration into temp
//            memcpy(listen_addresses_copy + num_listen_addresses, listen, num_addr);
//            num_listen_addresses += num_addr;
//            listen_addresses = malloc(num_listen_addresses * sizeof(char *));
//            CHECK_ALLOC(listen_addresses, "malloc")
//            //Append all addresses to global list
//            memcpy(listen_addresses, listen_addresses_copy, num_listen_addresses);
            //Free all allocated listen strings
            for (size_t i = 0; i < num_addr; i++) {
                free(*(listen + i));
            }
        }
    } while((fa = fa->prev));
    free_file_list(fa_ref);
    return status;
}

int main(int argc, char *argv[]) {
    if (argc == 1 || argc == 4) {
        systemd_invoked = argc == 4;
        if (systemd_invoked) {
            err_log = fopen("/dev/kmsg", "w");
            if (!err_log) {
                return -1;
            }
        } else {
            err_log = stderr;
        }
        const int r = gen_sslh_config(systemd_invoked ? argv[1] : "");
        if (systemd_invoked) {
            fclose(err_log);
        }
        if (!r) {
            fprintf(err_log, "systemd-sslh-generator: Successfully generated all targets.\n");
        }
        return r < 0 ? -1 : 0;
    } else {
        printf("This program takes three or no arguments.\n");
        return -1;
    }
}
