#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>


#define LOGS(lvl, fmt) \
    if ((lvl) >= LOG_LEVEL) fprintf(stderr, "dcrond: " fmt "\n")
#define LOGV(lvl, fmt, ...) \
    if ((lvl) >= LOG_LEVEL) fprintf(stderr, "dcrond: " fmt "\n", __VA_ARGS__)
#define LOGS_ERRNO(lvl, fmt) LOGV(lvl, fmt " - %d: %s", errno, strerror(errno))
#define LOGV_ERRNO(lvl, fmt, ...) LOGV(lvl, fmt " - %d: %s", __VA_ARGS__, errno, strerror(errno))


static unsigned LOG_LEVEL = 0;
static volatile int CAUGHT_SIGNAL = 0;
static volatile int CAUGHT_ALARM = 0;


/**
 * Set the first terminating signal caught to initiate shutdown.
 * Also set that the next alarm call has expired.
 */
static void signal_handler(int sig) {
    if (sig == SIGALRM) {
        ++CAUGHT_ALARM;
    } else if (sig != SIGCHLD && CAUGHT_SIGNAL == 0) {
        CAUGHT_SIGNAL = sig;
    }
}


/**
 * Register INT, QUIT, TERM and ALRM with exclusive handler mask and without restart.
 */
static int register_handlers() {
    struct sigaction sa = {0};
    sigemptyset(&sa.sa_mask);

    sa.sa_handler = SIG_IGN;
    sigaction(SIGHUP, &sa, NULL);
    sigaction(SIGPIPE, &sa, NULL);

    sigaddset(&sa.sa_mask, SIGINT);
    sigaddset(&sa.sa_mask, SIGQUIT);
    sigaddset(&sa.sa_mask, SIGTERM);
    sigaddset(&sa.sa_mask, SIGALRM);

    sa.sa_handler = &signal_handler;
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGQUIT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGALRM, &sa, NULL);

    return 0;
}


/**
 * Waiting for any signal received in the meanwhile.
 * This avoids the small race between checking for signals and entering a
 * blocking sleep() call.
 * Using signal- and pid-fd APIs or sigtimedwait as alternative solution is
 * not very POSIX/portable.
 * Passing the last checked signal stack generation allows to not block when
 * there are already changes pending.
 * Returning -1 in case of (not expected) errors or even integer overflows are
 * not really a problem here.
 */
static int signal_wait(int old_alarm) {
    sigset_t ss;
    sigemptyset(&ss);
    sigaddset(&ss, SIGINT);
    sigaddset(&ss, SIGQUIT);
    sigaddset(&ss, SIGTERM);
    sigaddset(&ss, SIGALRM);

    sigset_t ss_old;
    if (sigprocmask(SIG_BLOCK, &ss, &ss_old) == -1) {
        LOGS_ERRNO(1, "Cannot temporarily block signals");
        return -1;
    }
    if (CAUGHT_SIGNAL || CAUGHT_ALARM != old_alarm) {
        (void)sigprocmask(SIG_UNBLOCK, &ss, NULL);
        return CAUGHT_ALARM;
    }
    if (sigsuspend(&ss_old) == -1 && errno != EINTR) {
        LOGS_ERRNO(1, "Cannot suspend for signals");
        return -1;
    }
    if (sigprocmask(SIG_UNBLOCK, &ss, NULL) == -1) {
        LOGS_ERRNO(1, "Cannot unblock signals");
        return -1;
    }
    return CAUGHT_ALARM;
}


/**
 * Open /dev/null and dup() it to given file descriptor.
 * Prevents for example hanging reads from stdin, and simply closing could lead
 * to errors or confusion upon reassigning fd 0.
 * Nonzero upon error.
 */
static int dup_null(int fd, int mode) {
    const int nullfd = open("/dev/null", mode); // no CLOEXEC
    if (nullfd == -1) {
        LOGV_ERRNO(1, "Cannot open /dev/null for %d", fd);
        return -1;
    } else if (dup2(nullfd, fd) == -1) {
        LOGV_ERRNO(1, "Cannot dup /dev/null to %d", fd);
        return -1;
    } else if (close(nullfd) == -1) {
        LOGV_ERRNO(1, "Cannot close temporary /dev/null fd %d for %d", nullfd, fd);
        return -1;
    } else {
        return 0;
    }
}


/*
 * Main loop running the given command in intervals.
 */
static int run(unsigned delay, unsigned interval, int ignore_errors, const char* const command) {
    LOGV(0, "Starting up with %u+%us for command: %s", delay, interval, command);

    int alarm_generation = 0;
    if (delay > 0) {
        alarm(delay);
        alarm_generation = signal_wait(alarm_generation);
    }

    while (CAUGHT_SIGNAL == 0) {
        const int rv = system(command); // NOLINT(cert-env33-c)

        if (rv == -1) {
            LOGS_ERRNO(1, "Command shell failed, stopping");
            return 127;
        } else if (WIFSIGNALED(rv)) {
            if (ignore_errors) {
                LOGV(1, "Command exited due to signal %d, ignoring", WTERMSIG(rv));
            } else {
                LOGV(1, "Command exited due to signal %d, stopping", WTERMSIG(rv));
                return 128 + WTERMSIG(rv);
            }
        } else if (WIFEXITED(rv) && WEXITSTATUS(rv) != 0) {
            if (ignore_errors) {
                LOGV(1, "Command exited with status %d, ignoring", WEXITSTATUS(rv));
            } else {
                LOGV(1, "Command exited with status %d, stopping", WEXITSTATUS(rv));
                return WEXITSTATUS(rv);
            }
        } else if (rv != 0) {
            if (ignore_errors) {
                LOGV(1, "Command failed with status %d, ignoring", rv);
            } else {
                LOGV(1, "Command failed with status %d, stopping", rv);
                return rv < 255 ? rv : 255;
            }
        } else {
            LOGV(0, "Success: %s", command);
        }

        if (interval > 0) {
            alarm(interval);
            alarm_generation = signal_wait(alarm_generation);
        }
    }

    LOGV(0, "Received signal %d, stopping", CAUGHT_SIGNAL);
    return 0;
}


static int usage(const char* name) {
    LOGV(1, "Usage: %s [-q...] [-d delay] -i interval -- [-]commandline", name && *name ? name : "$0");
    return 1;
}


/**
 * Main, argument parsing.
 */
int main(int argc, char** argv) {
    int delay = 0;
    int interval = -1;
    int opt;
    while ((opt = getopt(argc, argv, "qd:i:")) != -1) {
        switch (opt) {
            case 'q':
                LOG_LEVEL++;
                break;
            case 'd':
                delay = atoi(optarg); // NOLINT(cert-err34-c)
                if (delay == 0 && strcmp(optarg, "0") != 0) return usage(argv[0]);
                break;
            case 'i':
                interval = atoi(optarg); // NOLINT(cert-err34-c)
                if (interval == 0 && strcmp(optarg, "0") != 0) return usage(argv[0]);
                break;
            default:
                return usage(argv[0]);
        }
    }

    if (optind + 1 != argc) {
        LOGS(1, "Expecting single commandline");
        return usage(argv[0]);
    }
    const char* command = argv[argc - 1];
    int ignore_errors = 0;
    if (command[0] == '-') {
        command++;
        ignore_errors = 1;
    }

    if (dup_null(0, O_RDONLY) != 0) {
        return 1;
    }
    if (LOG_LEVEL >= 3) {
        if (dup_null(1, O_WRONLY) != 0 || dup_null(2, O_WRONLY) != 0) {
            return 1;
        }
    }
    if (register_handlers() != 0) {
        return 1;
    }

    return run(delay >= 0 ? (unsigned)delay : 0, interval >= 0 ? (unsigned)interval : 0, ignore_errors, command);
}