#include "certd.hpp"


CONF_DEF(config) {
    ConfigKey cert_cache;
};
CONF_INIT(config) {
    CONF_KEY_INST(cert_cache, true, DiskCache, char*);
}
static Config* global_config = NULL;


static bool read_handler(int fd, char* cn, size_t cnlen) {
    DiskCache* cache = config? config->cert_cache.ctx_as<DiskCache>(): NULL;
    char* cert;
    size_t certlen;
    char* key;
    size_t keylen;
    tristate_t rv;

    char* hit = NULL;
    ssize_t hitlen = cache? cache->get(cn, hit): -1;
    if (hit) {
        log(debug, "'%s' cache hit", cn);
        rv = separator_write(fd, cn, cnlen, hit, hitlen);
        cache->pool.push(hit);
        return (rv == TRI_TRUE);
    } else if (mkcert(cn, cert, certlen, key, keylen)) {
        log(debug, "generated '%s'", cn);

        if (cache) {
            iovec iov[3];
            iov[0].iov_base = cert;
            iov[0].iov_len  = certlen;
            iov[1].iov_base = (void*)&"~";
            iov[1].iov_len  = 1;
            iov[2].iov_base = key;
            iov[2].iov_len  = keylen;
            (void)cache->set(cn, iov, 3, certlen + 1 + keylen);
        }

        rv = separator_write(fd, cn, cnlen, cert, certlen, key, keylen);
        free(cert);
        free(key);
        return (rv == TRI_TRUE);
    } else {
        log(error, "no cert for '%s'", cn);
        return (separator_write(fd, "", 0) == TRI_TRUE);
    }
}


int acceptor(int fd, msec_t tout) {
    if (tout && !has_data(fd, tout)) {
        return -1;
    }

    sockaddr_t src;
    int len = sizeof(src);
    int newfd = accept4(fd, (struct sockaddr*)&src, (socklen_t*)&len, SOCK_NONBLOCK); // non-blocking
    if (newfd == -1) {
        log_errno(error, "accept");
        return -1;
    }

    return newfd;
}


int main(int argc, char **argv) {
    EINTR_RETRY(close(STDIN_FILENO));
    EINTR_RETRY(close(STDOUT_FILENO));

    const char* config_file = (argc >= 2)? argv[1]: NULL;
    const char* socket_file = (argc >= 3)? argv[2]: NULL;
    if (!config_file || !socket_file) {
        STDERR("Usage: %s <config-file> <socket-file>", argv[0]);
        return 1;
    }
    Hooks::run(Hooks::INIT);
    global_config = new Config(config_file);
    if (!global_config->load()) {
        log(error, "cannot load config '%s'", config_file);
        delete global_config;
        Hooks::run(Hooks::DEINIT);
        return 1;
    }
    log(notice, "loaded config '%s'", config_file);

    Hooks::run(Hooks::PRE_IO);

    int acceptfd = uds_listen(socket_file);
    if (acceptfd == -1) {
        log(error, "cannot open socket '%s'", socket_file);
        delete global_config;
        Hooks::run(Hooks::DEINIT);
        return 1;
    }
    if (!set_blocking(acceptfd, false)) { // non-blocking
        delete global_config;
        Hooks::run(Hooks::DEINIT);
        return 1;
    }

    int conn = -1;
    while (!SHUTDOWN) {
        update_now(); // using timeout below so this gets updated
        if (conn == -1) {
            conn = acceptor(acceptfd, 1000);
        }
        if (conn != -1) {
            if (separator_read(conn, &read_handler, 1000) == TRI_FALSE) {
                EINTR_RETRY(close(conn)); // something seriously wrong with the fd or eof
                conn = -1;
            }
        }
        if (unlikely(RECONFIGURE)) {
            log(notice, "triggering config reload...");
            (void)global_config->load(); // blocking
            RECONFIGURE = 0;
            CLEANUP = 0; // implicitly done upon config change
        } else if (unlikely(CLEANUP)) {
            log(notice, "triggering cache clean...");
            Hooks::run(Hooks::CACHE_CLEAN);
            CLEANUP = 0;
        }
    }

    if (conn != -1) close(conn);
    close(acceptfd);
    if (unlink(socket_file) == -1) {
        log_errno(info, "unlink(%s)", socket_file);
    }
    Hooks::run(Hooks::POST_IO);
    delete global_config;
    Hooks::run(Hooks::DEINIT);
    return 0;
}