/*-
 * SPDX-FileCopyrightText: Peter Pentchev
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * stdsyslog - the main component of the utility for logging programs' output
 * to the system log
 */

#include <sys/types.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/wait.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#include "flexarr.h"

#ifndef __unused
#ifdef __GNUC__
#define __unused	__attribute__((unused))
#else
#define __unused
#endif
#endif

static const struct {
	const char	*name;
	int		 level;
} syslog_levels[] = {
#ifdef LOG_EMERG
	{"emerg",	LOG_EMERG},
#endif
#ifdef LOG_ALERT
	{"alert",	LOG_ALERT},
#endif
#ifdef LOG_CRIT
	{"crit",	LOG_CRIT},
#endif
#ifdef LOG_ERR
	{"err",		LOG_ERR},
#endif
#ifdef LOG_WARNING
	{"warning",	LOG_WARNING},
#endif
#ifdef LOG_NOTICE
	{"notice",	LOG_NOTICE},
#endif
#ifdef LOG_INFO
	{"info",	LOG_INFO},
#endif
#ifdef LOG_DEBUG
	{"debug",	LOG_DEBUG},
#endif
};
#define LEVELS_COUNT	(sizeof(syslog_levels) / sizeof(syslog_levels[0]))

static const struct {
	const char	*name;
	int		 facility;
} syslog_facilities[] = {
#ifdef LOG_AUTHPRIV
	{"authpriv",	LOG_AUTHPRIV},
#endif
#ifdef LOG_CRON
	{"cron",	LOG_CRON},
#endif
#ifdef LOG_DAEMON
	{"daemon",	LOG_DAEMON},
#endif
#ifdef LOG_FTP
	{"ftp",		LOG_FTP},
#endif
#ifdef LOG_KERN
	{"kern",	LOG_KERN},
#endif
#ifdef LOG_LOCAL0
	{"local0",	LOG_LOCAL0},
#endif
#ifdef LOG_LOCAL1
	{"local1",	LOG_LOCAL1},
#endif
#ifdef LOG_LOCAL2
	{"local2",	LOG_LOCAL2},
#endif
#ifdef LOG_LOCAL3
	{"local3",	LOG_LOCAL3},
#endif
#ifdef LOG_LOCAL4
	{"local4",	LOG_LOCAL4},
#endif
#ifdef LOG_LOCAL5
	{"local5",	LOG_LOCAL5},
#endif
#ifdef LOG_LOCAL6
	{"local6",	LOG_LOCAL6},
#endif
#ifdef LOG_LOCAL7
	{"local7",	LOG_LOCAL7},
#endif
#ifdef LOG_LPR
	{"lpr",		LOG_LPR},
#endif
#ifdef LOG_MAIL
	{"mail",	LOG_MAIL},
#endif
#ifdef LOG_NEWS
	{"news",	LOG_NEWS},
#endif
#ifdef LOG_SYSLOG
	{"syslog",	LOG_SYSLOG},
#endif
#ifdef LOG_USER
	{"user",	LOG_USER},
#endif
#ifdef LOG_UUCP
	{"uucp",	LOG_UUCP},
#endif
};
#define FACILITIES_COUNT	(sizeof(syslog_facilities) / sizeof(syslog_facilities[0]))

static const struct {
	const char	*name;
	int		 signal;
} signal_pass[] = {
#ifdef SIGHUP
	{"SIGHUP",	SIGHUP},
#endif
#ifdef SIGINT
	{"SIGINT",	SIGINT},
#endif
#ifdef SIGQUIT
	{"SIGQUIT",	SIGQUIT},
#endif
#ifdef SIGALRM
	{"SIGALRM",	SIGALRM},
#endif
#ifdef SIGUSR1
	{"SIGUSR1",	SIGUSR1},
#endif
#ifdef SIGUSR2
	{"SIGUSR2",	SIGUSR1},
#endif
#ifdef SIGTERM
	{"SIGTERM",	SIGTERM},
#endif
#ifdef SIGWINCH
	{"SIGWINCH",	SIGWINCH},
#endif
};
#define SIGNAL_PASS_COUNT	(sizeof(signal_pass) / sizeof(signal_pass[0]))

struct fdspec {
	int	 fd;
	int	 prio;
};

struct fdinfo {
	int	 fd;
	int	 pipe[2];
	char	*rbuf;
	size_t	 ralloc, rpos;
};

static struct fdspec fdspec_default[2] = {
	{1, LOG_INFO},
	{2, LOG_ERR},
};
#define FDSPEC_DEFAULT_SIZE	(sizeof(fdspec_default) / sizeof(fdspec_default[0]))

static char readbuf[2048];

static pid_t	 	pid;

static void	 usage(bool ferr);
static void	 version(void);

static void	 process_read(struct fdinfo *fd, char *readbuf, size_t sz,
		const struct fdspec *spec, size_t nspec);
static void	 process_line(const struct fdinfo *fd, const char *s,
		const struct fdspec *spec, size_t nspec);

static int	 parse_spec(struct fdspec *spec, const char *s);
static int	 syslog_facility(const char *s);

static void	 sig_pass(int sig);

int
main(int argc, char * const argv[])
{
	size_t nspec, allocspec;
	struct fdspec *spec;
	FLEXARR_INIT(spec, nspec, allocspec);

	bool hflag = false, Vflag = false;
	int facility = LOG_DAEMON;
	const char *pidfile = NULL;
	/* FIXME: more options, filters... */
	int ch;
	while ((ch = getopt(argc, argv, "d:f:hlp:V-:")) != -1) {
		switch (ch) {
			case 'd':
				{
				struct fdspec sp;
				if (parse_spec(&sp, optarg) == -1)
					return (1);
				FLEXARR_ALLOC(spec, 1, nspec, allocspec);
				spec[nspec - 1] = sp;
				}
				break;

			case 'f':
				facility = syslog_facility(optarg);
				if (facility == -1)
					errx(1,
					    "Unknown syslog facility '%s', "
					    "use '-f list' for a list",
					    optarg);
				break;

			case 'h':
				hflag = true;
				break;

			case 'l':
				puts("Available levels (priorities):");
				for (size_t i = 0; i < LEVELS_COUNT; i++)
					puts(syslog_levels[i].name);
				return (0);
				break;

			case 'p':
				pidfile = optarg;
				break;

			case 'V':
				Vflag = true;
				break;

			case '-':
				if (strcmp(optarg, "help") == 0)
					hflag = true;
				else if (strcmp(optarg, "version") == 0)
					Vflag = true;
				else {
					warnx("Invalid long option '%s' specified", optarg);
					usage(true);
				}
				break;

			default:
				usage(true);
				/* NOTREACHED */
		}
	}
	if (Vflag)
		version();
	if (hflag)
		usage(false);
	if (Vflag || hflag)
		return (0);
	argc -= optind;
	argv += optind;
	if (argc < 1)
		usage(true);

	/* FIXME: Filters, log, priority, etc. */
	openlog(basename(argv[0]), LOG_PID, facility);

	/* Set up the file descriptors */
	if (nspec == 0) {
		spec = fdspec_default;
		nspec = FDSPEC_DEFAULT_SIZE;
	}
	struct fdinfo *fds;
	size_t nfds, allocfds;
	FLEXARR_INIT(fds, nfds, allocfds);
	for (size_t i = 0; i < nspec; i++) {
		bool found = false;

		for (size_t j = 0; j < nfds; j++)
			if (fds[j].fd == spec[i].fd) {
				found = true;
				break;
			}
		if (found)
			continue;

		FLEXARR_ALLOC(fds, 1, nfds, allocfds);
		memset(fds + nfds - 1, 0, sizeof(*fds));
		fds[nfds - 1].fd = spec[i].fd;
		FLEXARR_INIT(fds[nfds - 1].rbuf, fds[nfds - 1].rpos,
		    fds[nfds - 1].ralloc);
	}

	/* Set up the pipes */
	for (size_t i = 0; i < nfds; i++)
		if (pipe(fds[i].pipe) == -1)
			err(1, "Could not create a communication pipe");

	/* Set up the signal handlers (parent only, but must do it early) */
	struct sigaction sa;
	memset(&sa, 0, sizeof(sa));
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = SA_RESTART;
	sa.sa_handler = sig_pass;
	for (size_t i = 0; i < SIGNAL_PASS_COUNT; i++)
		if (sigaction(signal_pass[i].signal, &sa, NULL) == -1)
			err(1, "Could not set up the handler for %s (%d)",
			    signal_pass[i].name, signal_pass[i].signal);

	FILE *pidfp;
	if (pidfile != NULL) {
		/**
		 * Ignore unlink() errors for the present.
		 * TODO: Add the -P pidfile option to run stalepid and,
		 * well, remove the process ID file if it is stale.
		 */
		unlink(pidfile);
		const int pidfd = open(pidfile, O_CREAT | O_EXCL | O_RDWR,
		    S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
		if (pidfd == -1)
			err(1, "Could not create the process ID file %s",
			    pidfile);
		if (pidfp = fdopen(pidfd, "w"), pidfp == NULL)
			err(1,
			    "Could not fdopen the process ID file %s at fd %d",
			    pidfile, pidfd);
	} else {
		pidfp = NULL;
	}

	pid = fork();
	if (pid == -1) {
		err(1, "Could not fork for %s", argv[0]);
	} else if (pid == 0) {
		if (pidfp != NULL) {
			fprintf(pidfp, "%jd\n", (intmax_t)getpid());
			fclose(pidfp);
		}

		for (size_t i = 0; i < nfds; i++) {
			close(fds[i].pipe[0]);
			close(fds[i].fd);
			if (dup2(fds[i].pipe[1], fds[i].fd) == -1) {
				fprintf((fds[i].fd == 2? stdout: stderr),
				    "Could not reopen file descriptor %d as our pipe writer %d: %s\n",
				    fds[i].fd, fds[i].pipe[1],
				    strerror(errno));
				exit(1);
			}

			/**
			 * Try to set line buffering, do not complain too
			 * loudly if it fails.
			 * Yes, this leaks FILE structures, but we really
			 * do not want to close the file descriptor!
			 */
			FILE * const fp = fdopen(fds[i].fd, "w");
			if (fp != NULL)
				setvbuf(fp, NULL, _IOLBF, 0);
		}
		execvp(argv[0], argv);
		err(1, "Could not execute %s", argv[0]);
		/* NOTREACHED */
	}
	if (pidfp != NULL)
		fclose(pidfp);

	/**
	 * Parent: close the write side of the pipe and set the read side
	 * to non-blocking.
	 */
	for (size_t i = 0; i < nfds; i++) {
		close(fds[i].pipe[1]);
		const int fd = fds[i].pipe[0];
		const int flags = fcntl(fd, F_GETFL, 0);
		if (flags == -1)
			err(1, "Could not obtain the flags for fd %d", fd);
		if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
			err(1, "Could not set the flags for fd %d", fd);
	}

	size_t left = nfds;
	while (left > 0) {
		fd_set rfd, efd;
		int maxfd;

		FD_ZERO(&rfd);
		FD_ZERO(&efd);
		maxfd = 0;
		for (size_t i = 0; i < nfds; i++) {
			const int fd = fds[i].pipe[0];

			if (fd == -1)
				continue;
			FD_SET(fd, &rfd);
			FD_SET(fd, &efd);
			if (fd > maxfd)
				maxfd = fd;
		}
		const int n = select(maxfd + 1, &rfd, NULL, &efd, NULL);
		if (n == -1) {
			if (errno == EINTR)
				continue;
			else
				err(1, "select() error with %zu fds left",
				    nfds);
		} else if (n < 1) {
			errx(1, "select(%zu) returned %d",
			    nfds, n);
		}
		for (size_t i = 0; i < nfds; i++) {
			const int fd = fds[i].pipe[0];
			if (fd == -1)
				continue;

			bool closefd = false;
			if (FD_ISSET(fd, &rfd)) {
				const ssize_t sz =
				    read(fd, readbuf, sizeof(readbuf));
				if (sz == -1)
					err(1, "read() error");
				else if (sz == 0)
					closefd = true;
				else
					process_read(fds + i, readbuf, sz, spec, nspec);
			}
			
			if (FD_ISSET(fd, &efd))
				closefd = true;

			if (closefd) {
				/* Close stuff and out */
				close(fd);
				fds[i].pipe[0] = -1;
				left--;
			}
		}
	}

	/* Oof, yes, this is a bit naive.  Want better event handling! */
	pid_t npid;
	int status;
	for (size_t i = 0; i < 3; i++) {
		npid = waitpid(pid, &status, 0);
		if (npid != -1 || errno != EINTR)
			break;
	}
	if (npid == -1)
		err(1, "Could not obtain the exit status of %s", argv[0]);
	else if (npid != pid)
		errx(1, "OS glitch: waitpid(%jd) returned pid %jd",
		    (intmax_t)pid, (intmax_t)npid);
	if (WIFEXITED(status))
		exit(WEXITSTATUS(status));
	else if (WIFSIGNALED(status))
		exit(WTERMSIG(status) + 128);
	else if (WIFSTOPPED(status))
		/* Nah, this really shouldn't happen, should it... */
		exit(WSTOPSIG(status) + 128);
	else
		errx(1, "The %s child process neither exited nor was killed "
		    "or stopped; what does wait() status %d mean?!",
		    argv[0], status);
	/* NOTREACHED */
}

static void
usage(const bool ferr)
{
	const char * const s =
	    "Usage:\tstdsyslog [-d fd:level] [-f facility] [-p pidfile] command [arg...]\n"
	    "\tstdsyslog -f list\n"
	    "\tstdsyslog -l\n"
	    "\tstdsyslog -V | -h\n\n"
	    "\t-d\tspecify the level for messages on a file descriptor (more than once);\n"
	    "\t-h\tdisplay program usage information and exit;\n"
	    "\t-f\tspecify the syslog facility to use (or 'list' for info);\n"
	    "\t-l\tlist the available syslog levels;\n"
	    "\t-p\tspecify the file to write the child process's ID to;\n"
	    "\t-V\tdisplay program version information and exit.\n\n"
	    "Examples:\n"
	    "\tstdsyslog -d 1:notice -d 2:crit sprog some args\n"
	    "\tstdsyslog -p sprog.pid -d 1:info -d 2:err -d 5:crit sprog more args\n";

	if (ferr) {
		fputs(s, stderr);
		exit(1);
	} else {
		printf("%s", s);
	}
}

static void
version(void)
{
	puts("stdsyslog 0.03.4");
}

static void
process_read(struct fdinfo * const fd, char * const rdbuf, const size_t sz,
		const struct fdspec * const spec, const size_t nspec)
{
	/* Bah, this is weird, but oh well */
	for (size_t i = 0; i < sz; i++)
		if (rdbuf[i] == '\0')
			rdbuf[i] = ' ';

	/* OK, tack it onto the end... */
	size_t cpos = fd->rpos;
	FLEXARR_ALLOC(fd->rbuf, sz, fd->rpos, fd->ralloc);
	memcpy(fd->rbuf + cpos, rdbuf, sz);

	/* We only need to look for newlines from cpos onwards */
	size_t linestart = 0;
	char *p;
	while (p = (char *)memchr(fd->rbuf + cpos, '\n', fd->rpos - cpos),
	    p != NULL) {
		*p = '\0';
		process_line(fd, fd->rbuf + linestart, spec, nspec);
		cpos = p + 1 - fd->rbuf;
		linestart = cpos;
		if (cpos == fd->rpos) {
			break;
		} else if (cpos > fd->rpos) {
			errx(1, "INTERNAL ERROR: fd %d: cpos overflow, cpos %zu rpos %zu rbuf %p p %p ralloc %zu\n", fd->fd, cpos, fd->rpos, fd->rbuf, p, fd->ralloc);
		}
	}
	if (linestart > 0) {
		memmove(fd->rbuf, fd->rbuf + linestart, fd->rpos - linestart);
		fd->rpos -= linestart;
	}
}

static void
process_line(const struct fdinfo * const fd, const char * const s,
		const struct fdspec * const spec, const size_t nspec)
{
	size_t i;

	/* FIXME: Rules, prefixes, etc. */
	for (i = 0; i < nspec; i++)
		if (spec[i].fd == fd->fd)
			break;
	if (i == nspec)
		errx(1, "INTERNAL ERROR: process_line(): no match on fd %d, line %s", fd->fd, s);

	syslog(spec[i].prio, "%s", s);
}

static int
syslog_facility(const char * const s)
{
	if (!strcmp(s, "list")) {
		puts("Available facilities:");
		for (size_t i = 0; i < FACILITIES_COUNT; i++)
			puts(syslog_facilities[i].name);
		exit(0);
	}
	
	for (size_t i = 0; i < FACILITIES_COUNT; i++)
		if (!strcmp(s, syslog_facilities[i].name))
			return (syslog_facilities[i].facility);
	return (-1);
}

static int
parse_spec(struct fdspec * const spec, const char * const s)
{
	struct fdspec sp;
	memset(&sp, 0, sizeof(sp));

	char * const copy = strdup(s);
	if (copy == NULL)
		err(1, "Could not copy an option string");
	char *tokctx;
	char *part = strtok_r(copy, ":", &tokctx);
	if (part == NULL)
		errx(1, "No file descriptor specified for -d");
	char *end;
	const long long lfd = strtoll(part, &end, 10);
	if (*part == '\0' || *end != '\0' || lfd < 0 || lfd > INT_MAX)
		errx(1, "Invalid file descriptor specified for -d");
	sp.fd = lfd;

	part = strtok_r(NULL, ":", &tokctx);
	if (part == NULL || *part == '\0')
		errx(1, "No log priority specified for -d");
	size_t i;
	for (i = 0; i < LEVELS_COUNT; i++)
		if (!strcmp(syslog_levels[i].name, part))
			break;
	if (i == LEVELS_COUNT)
		errx(1, "Invalid log priority specified for -d");
	sp.prio = syslog_levels[i].level;

	part = strtok_r(NULL, ":", &tokctx);
	if (part != NULL)
		errx(1, "Extra data at the end of the -d specification; "
		    "just fd:prio supported so far");

	memcpy(spec, &sp, sizeof(*spec));
	free(copy);
	return (0);
}

static void
sig_pass(const int sig)
{
	if (pid != 0)
		kill(pid, sig);
}
