/*
   Logging utilities

   Copyright (C) Andrew Tridgell  2008
   Copyright (C) Martin Schwenke  2014
   Copyright (C) Amitay Isaacs  2015

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, see <http://www.gnu.org/licenses/>.
*/

#include "replace.h"
#include "system/network.h"
#include "system/locale.h"
#include "system/time.h"
#include "system/filesys.h"
#include "system/syslog.h"
#include "system/dir.h"

#include "lib/util/time_basic.h"
#include "lib/util/sys_rw.h"
#include "lib/util/debug.h"
#include "lib/util/blocking.h"
#include "lib/util/samba_util.h" /* get_myname() */

#include "common/logging.h"

struct {
	int log_level;
	const char *log_string;
} log_string_map[] = {
	{ DEBUG_ERR,     "ERROR" },
	{ DEBUG_WARNING, "WARNING" },
	{ 2,             "WARNING" },
	{ DEBUG_NOTICE,  "NOTICE" },
	{ 4,             "NOTICE" },
	{ DEBUG_INFO,    "INFO" },
	{ 6,             "INFO" },
	{ 7,             "INFO" },
	{ 8,             "INFO" },
	{ 9,             "INFO" },
	{ DEBUG_DEBUG,   "DEBUG" },
};

bool debug_level_parse(const char *log_string, int *log_level)
{
	size_t i;

	if (log_string == NULL) {
		return false;
	}

	if (isdigit(log_string[0])) {
		int level = atoi(log_string);

		if (level >= 0 && (size_t)level < ARRAY_SIZE(log_string_map)) {
			*log_level = level;
			return true;
		}
		return false;
	}

	for (i=0; i<ARRAY_SIZE(log_string_map); i++) {
		if (strncasecmp(log_string_map[i].log_string,
				log_string, strlen(log_string)) == 0) {
			*log_level = log_string_map[i].log_level;
			return true;
		}
	}

	return false;
}

const char *debug_level_to_string(int log_level)
{
	size_t i;

	for (i=0; i < ARRAY_SIZE(log_string_map); i++) {
		if (log_string_map[i].log_level == log_level) {
			return log_string_map[i].log_string;
		}
	}
	return "UNKNOWN";
}

int debug_level_from_string(const char *log_string)
{
	bool found;
	int log_level;

	found = debug_level_parse(log_string, &log_level);
	if (found) {
		return log_level;
	}

	/* Default debug level */
	return DEBUG_ERR;
}

/*
 * file logging backend
 */

static bool file_log_validate(const char *option)
{
	char *t, *dir;
	struct stat st;
	int ret;

	if (option == NULL || strcmp(option, "-") == 0) {
		return true;
	}

	t = strdup(option);
	if (t == NULL) {
		return false;
	}

	dir = dirname(t);

	ret = stat(dir, &st);
	free(t);
	if (ret != 0) {
		return false;
	}

	if (! S_ISDIR(st.st_mode)) {
		return false;
	}

	return true;
}

static int file_log_setup(TALLOC_CTX *mem_ctx,
			  const char *option,
			  const char *app_name)
{
	struct debug_settings settings = {
		.debug_syslog_format = DEBUG_SYSLOG_FORMAT_ALWAYS,
		.debug_hires_timestamp = true,
		.debug_no_stderr_redirect = true,
	};
	const char *t = NULL;

	if (option == NULL || strcmp(option, "-") == 0) {
		/*
		 * Logging to stderr is the default and has already
		 * been done in logging init
		 */
		return 0;
	}

	/*
	 * Support logging of fake hostname in local daemons.  This
	 * hostname is basename(getenv(CTDB_BASE)).
	 */
	t = getenv("CTDB_TEST_MODE");
	if (t != NULL) {
		t = getenv("CTDB_BASE");
		if (t != NULL) {
			const char *p = strrchr(t, '/');
			if (p != NULL) {
				p++;
				if (p[0] == '\0') {
					p = "unknown";
				}
			} else {
				p = t;
			}

			debug_set_hostname(p);
		}
	}

	debug_set_settings(&settings, "file", 0, false);
	debug_set_logfile(option);
	setup_logging(app_name, DEBUG_FILE);

	return 0;
}

/*
 * syslog logging backend
 */

/* Copied from lib/util/debug.c */
static int debug_level_to_priority(int level)
{
        /*
         * map debug levels to syslog() priorities
         */
        static const int priority_map[] = {
                LOG_ERR,     /* 0 */
                LOG_WARNING, /* 1 */
                LOG_NOTICE,  /* 2 */
                LOG_NOTICE,  /* 3 */
                LOG_NOTICE,  /* 4 */
                LOG_NOTICE,  /* 5 */
                LOG_INFO,    /* 6 */
                LOG_INFO,    /* 7 */
                LOG_INFO,    /* 8 */
                LOG_INFO,    /* 9 */
        };
        int priority;

	if ((size_t)level >= ARRAY_SIZE(priority_map) || level < 0) {
		priority = LOG_DEBUG;
	} else {
		priority = priority_map[level];
	}
	return priority;
}

struct syslog_log_state {
	int fd;
	const char *app_name;
	const char *hostname;
	int (*format)(int dbglevel, struct syslog_log_state *state,
		      const char *str, char *buf, int bsize);
	/* RFC3164 says: The total length of the packet MUST be 1024
	   bytes or less. */
	char buffer[1024];
	unsigned int dropped_count;
};

/* Format messages as per RFC3164
 *
 * It appears that some syslog daemon implementations do not allow a
 * hostname when messages are sent via a Unix domain socket, so omit
 * it.  Similarly, syslogd on FreeBSD does not understand the hostname
 * part of the header, even when logging via UDP.  Note that most
 * implementations will log messages against "localhost" when logging
 * via UDP.  A timestamp could be sent but rsyslogd on Linux limits
 * the timestamp logged to the precision that was received on
 * /dev/log.  It seems sane to send degenerate RFC3164 messages
 * without a header at all, so that the daemon will generate high
 * resolution timestamps if configured.
 */
static int format_rfc3164(int dbglevel, struct syslog_log_state *state,
			  const char *str, char *buf, int bsize)
{
	int pri;
	int len;

	pri = LOG_DAEMON | debug_level_to_priority(dbglevel);
	len = snprintf(buf, bsize, "<%d>%s[%u]: %s",
		       pri, state->app_name, getpid(), str);
	buf[bsize-1] = '\0';
	len = MIN(len, bsize - 1);

	return len;
}

/* Format messages as per RFC5424
 *
 * <165>1 2003-08-24T05:14:15.000003-07:00 192.0.2.1
 *         myproc 8710 - - %% It's time to make the do-nuts.
 */
static int format_rfc5424(int dbglevel, struct syslog_log_state *state,
			  const char *str, char *buf, int bsize)
{
	int pri;
	struct timeval tv;
	struct timeval_buf tvbuf;
	int len, s;

	/* Header */
	pri = LOG_DAEMON | debug_level_to_priority(dbglevel);
	GetTimeOfDay(&tv);
	len = snprintf(buf, bsize,
		       "<%d>1 %s %s %s %u - - ",
		       pri, timeval_str_buf(&tv, true, true, &tvbuf),
		       state->hostname, state->app_name, getpid());
	/* A truncated header is not useful... */
	if (len >= bsize) {
		return -1;
	}

	/* Message */
	s = snprintf(&buf[len], bsize - len, "%s", str);
	buf[bsize-1] = '\0';
	len = MIN(len + s, bsize - 1);

	return len;
}

static void syslog_log(void *private_data, int level, const char *msg)
{
	syslog(debug_level_to_priority(level), "%s", msg);
}

static int syslog_log_sock_maybe(struct syslog_log_state *state,
				 int level, const char *msg)
{
	int n;
	ssize_t ret;

	n = state->format(level, state, msg, state->buffer,
			  sizeof(state->buffer));
	if (n == -1) {
		return E2BIG;
	}

	do {
		ret = write(state->fd, state->buffer, n);
	} while (ret == -1 && errno == EINTR);

	if (ret == -1) {
		return errno;
	}

	return 0;

}
static void syslog_log_sock(void *private_data, int level, const char *msg)
{
	struct syslog_log_state *state = talloc_get_type_abort(
		private_data, struct syslog_log_state);
	int ret;

	if (state->dropped_count > 0) {
		char t[64] = { 0 };
		snprintf(t, sizeof(t),
			 "[Dropped %u log messages]\n",
			 state->dropped_count);
		t[sizeof(t)-1] = '\0';
		ret = syslog_log_sock_maybe(state, level, t);
		if (ret == EAGAIN || ret == EWOULDBLOCK) {
			state->dropped_count++;
			/*
			 * If above failed then actually drop the
			 * message that would be logged below, since
			 * it would have been dropped anyway and it is
			 * also likely to fail.  Falling through and
			 * attempting to log the message also means
			 * that the dropped message count will be
			 * logged out of order.
			 */
			return;
		}
		if (ret != 0) {
			/* Silent failure on any other error */
			return;
		}
		state->dropped_count = 0;
	}

	ret = syslog_log_sock_maybe(state, level, msg);
	if (ret == EAGAIN || ret == EWOULDBLOCK) {
		state->dropped_count++;
	}
}

static int syslog_log_setup_syslog(TALLOC_CTX *mem_ctx, const char *app_name)
{
	openlog(app_name, LOG_PID, LOG_DAEMON);

	debug_set_callback(NULL, syslog_log);

	return 0;
}

static int syslog_log_state_destructor(struct syslog_log_state *state)
{
	if (state->fd != -1) {
		close(state->fd);
		state->fd = -1;
	}
	return 0;
}

static int syslog_log_setup_common(TALLOC_CTX *mem_ctx, const char *app_name,
				   struct syslog_log_state **result)
{
	struct syslog_log_state *state;

	state = talloc_zero(mem_ctx, struct syslog_log_state);
	if (state == NULL) {
		return ENOMEM;
	}

	state->fd = -1;
	state->app_name = app_name;
	talloc_set_destructor(state, syslog_log_state_destructor);

	*result = state;
	return 0;
}

#ifdef _PATH_LOG
static int syslog_log_setup_nonblocking(TALLOC_CTX *mem_ctx,
					const char *app_name)
{
	struct syslog_log_state *state = NULL;
	struct sockaddr_un dest;
	int ret;

	ret = syslog_log_setup_common(mem_ctx, app_name, &state);
	if (ret != 0) {
		return ret;
	}

	state->fd = socket(AF_UNIX, SOCK_DGRAM, 0);
	if (state->fd == -1) {
		int save_errno = errno;
		talloc_free(state);
		return save_errno;
	}

	dest.sun_family = AF_UNIX;
	strncpy(dest.sun_path, _PATH_LOG, sizeof(dest.sun_path)-1);
	ret = connect(state->fd,
		      (struct sockaddr *)&dest, sizeof(dest));
	if (ret == -1) {
		int save_errno = errno;
		talloc_free(state);
		return save_errno;
	}

	ret = set_blocking(state->fd, false);
	if (ret != 0) {
		int save_errno = errno;
		talloc_free(state);
		return save_errno;
	}

	if (! set_close_on_exec(state->fd)) {
		int save_errno = errno;
		talloc_free(state);
		return save_errno;
	}

	state->hostname = NULL; /* Make this explicit */
	state->format = format_rfc3164;

	debug_set_callback(state, syslog_log_sock);

	return 0;
}
#endif /* _PATH_LOG */

static int syslog_log_setup_udp(TALLOC_CTX *mem_ctx, const char *app_name,
				bool rfc5424)
{
	struct syslog_log_state *state = NULL;
	struct sockaddr_in dest;
	int ret;

	ret = syslog_log_setup_common(mem_ctx, app_name, &state);
	if (ret != 0) {
		return ret;
	}

	state->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (state->fd == -1) {
		int save_errno = errno;
		talloc_free(state);
		return save_errno;
	}

	dest.sin_family = AF_INET;
	dest.sin_port   = htons(514);
	dest.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	ret = connect(state->fd,
		      (struct sockaddr *)&dest, sizeof(dest));
	if (ret == -1) {
		int save_errno = errno;
		talloc_free(state);
		return save_errno;
	}

	if (! set_close_on_exec(state->fd)) {
		int save_errno = errno;
		talloc_free(state);
		return save_errno;
	}

	state->hostname = get_myname(state);
	if (state->hostname == NULL) {
		/* Use a fallback instead of failing initialisation */
		state->hostname = "localhost";
	}
	if (rfc5424) {
		state->format = format_rfc5424;
	} else {
		state->format = format_rfc3164;
	}

	debug_set_callback(state, syslog_log_sock);

	return 0;
}

static bool syslog_log_validate(const char *option)
{
	if (option == NULL) {
		return true;
#ifdef _PATH_LOG
	} else if (strcmp(option, "nonblocking") == 0) {
		return true;
#endif
	} else if (strcmp(option, "udp") == 0) {
		return true;
	} else if (strcmp(option, "udp-rfc5424") == 0) {
		return true;
	}

	return false;
}

static int syslog_log_setup(TALLOC_CTX *mem_ctx, const char *option,
			    const char *app_name)
{
	if (option == NULL) {
		return syslog_log_setup_syslog(mem_ctx, app_name);
#ifdef _PATH_LOG
	} else if (strcmp(option, "nonblocking") == 0) {
		return syslog_log_setup_nonblocking(mem_ctx, app_name);
#endif
	} else if (strcmp(option, "udp") == 0) {
		return syslog_log_setup_udp(mem_ctx, app_name, false);
	} else if (strcmp(option, "udp-rfc5424") == 0) {
		return syslog_log_setup_udp(mem_ctx, app_name, true);
	}

	return EINVAL;
}

struct log_backend {
	const char *name;
	bool (*validate)(const char *option);
	int (*setup)(TALLOC_CTX *mem_ctx,
		     const char *option,
		     const char *app_name);
};

static struct log_backend log_backend[] = {
	{
		.name = "file",
		.validate = file_log_validate,
		.setup = file_log_setup,
	},
	{
		.name = "syslog",
		.validate = syslog_log_validate,
		.setup = syslog_log_setup,
	},
};

static int log_backend_parse(TALLOC_CTX *mem_ctx,
			     const char *logging,
			     struct log_backend **backend,
			     char **backend_option)
{
	struct log_backend *b = NULL;
	char *t, *name, *option;
	size_t i;

	t = talloc_strdup(mem_ctx, logging);
	if (t == NULL) {
		return ENOMEM;
	}

	name = strtok(t, ":");
	if (name == NULL) {
		talloc_free(t);
		return EINVAL;
	}
	option = strtok(NULL, ":");

	for (i=0; i<ARRAY_SIZE(log_backend); i++) {
		if (strcmp(log_backend[i].name, name) == 0) {
			b = &log_backend[i];
		}
	}

	if (b == NULL) {
		talloc_free(t);
		return ENOENT;
	}

	*backend = b;
	if (option != NULL) {
		*backend_option = talloc_strdup(mem_ctx, option);
		if (*backend_option == NULL) {
			talloc_free(t);
			return ENOMEM;
		}
	} else {
		*backend_option = NULL;
	}

	talloc_free(t);
	return 0;
}

bool logging_validate(const char *logging)
{
	TALLOC_CTX *tmp_ctx;
	struct log_backend *backend;
	char *option;
	int ret;
	bool status;

	tmp_ctx = talloc_new(NULL);
	if (tmp_ctx == NULL) {
		return false;
	}

	ret = log_backend_parse(tmp_ctx, logging, &backend, &option);
	if (ret != 0) {
		talloc_free(tmp_ctx);
		return false;
	}

	status = backend->validate(option);
	talloc_free(tmp_ctx);
	return status;
}

/* Initialise logging */
int logging_init(TALLOC_CTX *mem_ctx, const char *logging,
		 const char *debug_level, const char *app_name)
{
	struct log_backend *backend = NULL;
	char *option = NULL;
	int level;
	int ret;

	setup_logging(app_name, DEBUG_DEFAULT_STDERR);

	if (debug_level == NULL) {
		debug_level = getenv("CTDB_DEBUGLEVEL");
	}
	if (! debug_level_parse(debug_level, &level)) {
		return EINVAL;
	}
	debuglevel_set(level);

	if (logging == NULL) {
		logging = getenv("CTDB_LOGGING");
	}
	if (logging == NULL || logging[0] == '\0') {
		return EINVAL;
	}

	ret = log_backend_parse(mem_ctx, logging, &backend, &option);
	if (ret != 0) {
		if (ret == ENOENT) {
			fprintf(stderr, "Invalid logging option \'%s\'\n",
				logging);
		}
		talloc_free(option);
		return ret;
	}

	ret = backend->setup(mem_ctx, option, app_name);
	talloc_free(option);
	return ret;
}

bool logging_reopen_logs(void)
{
	bool status;

	status = reopen_logs_internal();

	return status;
}

struct logging_reopen_logs_data {
	void (*hook)(void *private_data);
	void *private_data;
};

static void logging_sig_hup_handler(struct tevent_context *ev,
				    struct tevent_signal *se,
				    int signum,
				    int count,
				    void *dont_care,
				    void *private_data)
{
	bool status;

	if (private_data != NULL) {
		struct logging_reopen_logs_data *data = talloc_get_type_abort(
			private_data, struct logging_reopen_logs_data);

		if (data->hook != NULL) {
			data->hook(data->private_data);
		}
	}

	status = logging_reopen_logs();
	if (!status) {
		D_WARNING("Failed to reopen logs\n");
		return;
	}

	D_NOTICE("Reopened logs\n");

}

bool logging_setup_sighup_handler(struct tevent_context *ev,
				  TALLOC_CTX *talloc_ctx,
				  void (*hook)(void *private_data),
				  void *private_data)
{
	struct logging_reopen_logs_data *data = NULL;
	struct tevent_signal *se;

	if (hook != NULL) {
		data = talloc(talloc_ctx, struct logging_reopen_logs_data);
		if (data == NULL) {
			return false;
		}

		data->hook = hook;
		data->private_data = private_data;
	}


	se = tevent_add_signal(ev,
			       talloc_ctx,
			       SIGHUP,
			       0,
			       logging_sig_hup_handler,
			       data);
	if (se == NULL) {
		talloc_free(data);
		return false;
	}

	return true;
}
