/*
 * Copyright 2023 Vsevolod Stakhov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "config.h"
#include "logger.h"
#include "libserver/cfg_file.h"
#include "libcryptobox/cryptobox.h"
#include "unix-std.h"

#include "logger_private.h"

#define FILE_LOG_QUARK g_quark_from_static_string("file_logger")

struct rspamd_file_logger_priv {
	gint fd;
	struct {
		guint32 size;
		guint32 used;
		u_char *buf;
	} io_buf;
	gboolean throttling;
	gchar *log_file;
	gboolean is_buffered;
	gboolean log_severity;
	time_t throttling_time;
	guint32 repeats;
	guint64 last_line_cksum;
	gchar *saved_message;
	gsize saved_mlen;
	gchar *saved_function;
	gchar *saved_module;
	gchar *saved_id;
	guint saved_loglevel;
};

/**
 * Calculate checksum for log line (used for repeating logic)
 */
static inline guint64
rspamd_log_calculate_cksum(const gchar *message, size_t mlen)
{
	return rspamd_cryptobox_fast_hash(message, mlen, rspamd_hash_seed());
}

/*
 * Write a line to log file (unbuffered)
 */
static bool
direct_write_log_line(rspamd_logger_t *rspamd_log,
					  struct rspamd_file_logger_priv *priv,
					  void *data,
					  gsize count,
					  gboolean is_iov,
					  gint level_flags)
{
	struct iovec *iov;
	const gchar *line;
	glong r;
	gint fd;
	gboolean locked = FALSE;

	iov = (struct iovec *) data;
	fd = priv->fd;

	if (!rspamd_log->no_lock) {
		gsize tlen;

		if (is_iov) {
			tlen = 0;

			for (guint i = 0; i < count; i++) {
				tlen += iov[i].iov_len;
			}
		}
		else {
			tlen = count;
		}

		if (tlen > PIPE_BUF) {
			locked = TRUE;

#ifndef DISABLE_PTHREAD_MUTEX
			if (rspamd_log->mtx) {
				rspamd_mempool_lock_mutex(rspamd_log->mtx);
			}
			else {
				rspamd_file_lock(fd, FALSE);
			}
#else
			rspamd_file_lock(fd, FALSE);
#endif
		}
	}

	if (is_iov) {
		r = writev(fd, iov, count);
	}
	else {
		line = (const gchar *) data;
		r = write(fd, line, count);
	}

	if (locked) {
#ifndef DISABLE_PTHREAD_MUTEX
		if (rspamd_log->mtx) {
			rspamd_mempool_unlock_mutex(rspamd_log->mtx);
		}
		else {
			rspamd_file_unlock(fd, FALSE);
		}
#else
		rspamd_file_unlock(fd, FALSE);
#endif
	}

	if (r == -1) {
		/* We cannot write message to file, so we need to detect error and make decision */
		if (errno == EINTR) {
			/* Try again */
			return direct_write_log_line(rspamd_log, priv, data, count, is_iov, level_flags);
		}

		if (errno == EFAULT || errno == EINVAL || errno == EFBIG ||
			errno == ENOSPC) {
			/* Rare case */
			priv->throttling = TRUE;
			priv->throttling_time = time(NULL);
		}
		else if (errno == EPIPE || errno == EBADF) {
			/* We write to some pipe and it disappears, disable logging or we has opened bad file descriptor */
			rspamd_log->enabled = FALSE;
		}

		return false;
	}
	else if (priv->throttling) {
		priv->throttling = FALSE;
	}

	return true;
}

/**
 * Fill buffer with message (limits must be checked BEFORE this call)
 */
static void
fill_buffer(rspamd_logger_t *rspamd_log,
			struct rspamd_file_logger_priv *priv,
			const struct iovec *iov, gint iovcnt)
{
	gint i;

	for (i = 0; i < iovcnt; i++) {
		memcpy(priv->io_buf.buf + priv->io_buf.used,
			   iov[i].iov_base,
			   iov[i].iov_len);
		priv->io_buf.used += iov[i].iov_len;
	}
}

static void
rspamd_log_flush(rspamd_logger_t *rspamd_log, struct rspamd_file_logger_priv *priv)
{
	if (priv->is_buffered) {
		direct_write_log_line(rspamd_log,
							  priv,
							  priv->io_buf.buf,
							  priv->io_buf.used,
							  FALSE,
							  rspamd_log->log_level);
		priv->io_buf.used = 0;
	}
}

/*
 * Write message to buffer or to file (using direct_write_log_line function)
 */
static bool
file_log_helper(rspamd_logger_t *rspamd_log,
				struct rspamd_file_logger_priv *priv,
				const struct iovec *iov,
				guint iovcnt,
				gint level_flags)
{
	size_t len = 0;
	guint i;

	if (!priv->is_buffered) {
		/* Write string directly */
		return direct_write_log_line(rspamd_log, priv, (void *) iov, iovcnt,
									 TRUE, level_flags);
	}
	else {
		/* Calculate total length */
		for (i = 0; i < iovcnt; i++) {
			len += iov[i].iov_len;
		}
		/* Fill buffer */
		if (priv->io_buf.size < len) {
			/* Buffer is too small to hold this string, so write it directly */
			rspamd_log_flush(rspamd_log, priv);
			return direct_write_log_line(rspamd_log, priv, (void *) iov, iovcnt,
										 TRUE, level_flags);
		}
		else if (priv->io_buf.used + len >= priv->io_buf.size) {
			/* Buffer is full, try to write it directly */
			rspamd_log_flush(rspamd_log, priv);
			fill_buffer(rspamd_log, priv, iov, iovcnt);
		}
		else {
			/* Copy incoming string to buffer */
			fill_buffer(rspamd_log, priv, iov, iovcnt);
		}
	}

	return true;
}

static void
rspamd_log_reset_repeated(rspamd_logger_t *rspamd_log,
						  struct rspamd_file_logger_priv *priv)
{
	gchar tmpbuf[256];
	gssize r;

	if (priv->repeats > REPEATS_MIN) {
		r = rspamd_snprintf(tmpbuf,
							sizeof(tmpbuf),
							"Last message repeated %ud times",
							priv->repeats - REPEATS_MIN);
		priv->repeats = 0;

		if (priv->saved_message) {
			rspamd_log_file_log(priv->saved_module,
								priv->saved_id,
								priv->saved_function,
								priv->saved_loglevel | RSPAMD_LOG_FORCED,
								priv->saved_message,
								priv->saved_mlen,
								rspamd_log,
								priv);

			g_free(priv->saved_message);
			g_free(priv->saved_function);
			g_free(priv->saved_module);
			g_free(priv->saved_id);
			priv->saved_message = NULL;
			priv->saved_function = NULL;
			priv->saved_module = NULL;
			priv->saved_id = NULL;
		}

		/* It is safe to use temporary buffer here as it is not static */
		rspamd_log_file_log(NULL, NULL,
							G_STRFUNC,
							priv->saved_loglevel | RSPAMD_LOG_FORCED,
							tmpbuf,
							r,
							rspamd_log,
							priv);
		rspamd_log_flush(rspamd_log, priv);
	}
}

static gint
rspamd_try_open_log_fd(rspamd_logger_t *rspamd_log,
					   struct rspamd_file_logger_priv *priv,
					   uid_t uid, gid_t gid,
					   GError **err)
{
	gint fd;

	fd = open(priv->log_file,
			  O_CREAT | O_WRONLY | O_APPEND,
			  S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH);
	if (fd == -1) {
		g_set_error(err, FILE_LOG_QUARK, errno,
					"open_log: cannot open desired log file: %s, %s\n",
					priv->log_file, strerror(errno));
		return -1;
	}

	if (uid != -1 || gid != -1) {
		if (fchown(fd, uid, gid) == -1) {
			g_set_error(err, FILE_LOG_QUARK, errno,
						"open_log: cannot chown desired log file: %s, %s\n",
						priv->log_file, strerror(errno));
			close(fd);

			return -1;
		}
	}

	return fd;
}

void *
rspamd_log_file_init(rspamd_logger_t *logger, struct rspamd_config *cfg,
					 uid_t uid, gid_t gid, GError **err)
{
	struct rspamd_file_logger_priv *priv;

	if (!cfg || !cfg->cfg_name) {
		g_set_error(err, FILE_LOG_QUARK, EINVAL,
					"no log file specified");
		return NULL;
	}

	priv = g_malloc0(sizeof(*priv));

	if (cfg->log_buffered) {
		if (cfg->log_buf_size != 0) {
			priv->io_buf.size = cfg->log_buf_size;
		}
		else {
			priv->io_buf.size = LOGBUF_LEN;
		}
		priv->is_buffered = TRUE;
		priv->io_buf.buf = g_malloc(priv->io_buf.size);
	}

	if (cfg->log_file) {
		priv->log_file = g_strdup(cfg->log_file);
	}

	priv->log_severity = (logger->flags & RSPAMD_LOG_FLAG_SEVERITY);
	priv->fd = rspamd_try_open_log_fd(logger, priv, uid, gid, err);

	if (priv->fd == -1) {
		rspamd_log_file_dtor(logger, priv);

		return NULL;
	}

	return priv;
}

void rspamd_log_file_dtor(rspamd_logger_t *logger, gpointer arg)
{
	struct rspamd_file_logger_priv *priv = (struct rspamd_file_logger_priv *) arg;

	rspamd_log_reset_repeated(logger, priv);
	rspamd_log_flush(logger, priv);

	if (priv->fd != -1) {
		if (close(priv->fd) == -1) {
			rspamd_fprintf(stderr, "cannot close log fd %d: %s; log file = %s\n",
						   priv->fd, strerror(errno), priv->log_file);
		}
	}

	g_free(priv->log_file);
	g_free(priv);
}

bool rspamd_log_file_log(const gchar *module, const gchar *id,
						 const gchar *function,
						 gint level_flags,
						 const gchar *message,
						 gsize mlen,
						 rspamd_logger_t *rspamd_log,
						 gpointer arg)
{
	struct rspamd_file_logger_priv *priv = (struct rspamd_file_logger_priv *) arg;
	gdouble now;
	guint64 cksum;
	gboolean got_time = FALSE;


	if (!(level_flags & RSPAMD_LOG_FORCED) && !rspamd_log->enabled) {
		return false;
	}

	/* Check throttling due to write errors */
	if (!(level_flags & RSPAMD_LOG_FORCED) && priv->throttling) {
		now = rspamd_get_calendar_ticks();

		if (priv->throttling_time != now) {
			priv->throttling_time = now;
			got_time = TRUE;
		}
		else {
			/* Do not try to write to file too often while throttling */
			return false;
		}
	}

	/* Check repeats */
	cksum = rspamd_log_calculate_cksum(message, mlen);

	if (cksum == priv->last_line_cksum) {
		priv->repeats++;

		if (priv->repeats > REPEATS_MIN && priv->repeats <
											   REPEATS_MAX) {
			/* Do not log anything but save message for future */
			if (priv->saved_message == NULL) {
				priv->saved_function = g_strdup(function);
				priv->saved_mlen = mlen;
				priv->saved_message = g_malloc(mlen);
				memcpy(priv->saved_message, message, mlen);

				if (module) {
					priv->saved_module = g_strdup(module);
				}

				if (id) {
					priv->saved_id = g_strdup(id);
				}

				priv->saved_loglevel = level_flags;
			}

			return true;
		}
		else if (priv->repeats > REPEATS_MAX) {
			rspamd_log_reset_repeated(rspamd_log, priv);

			bool ret = rspamd_log_file_log(module, id,
										   function,
										   level_flags,
										   message,
										   mlen,
										   rspamd_log,
										   priv);

			/* Probably we have more repeats in future */
			priv->repeats = REPEATS_MIN + 1;

			return ret;
		}
	}
	else {
		/* Reset counter if new message differs from saved message */
		priv->last_line_cksum = cksum;

		if (priv->repeats > REPEATS_MIN) {
			rspamd_log_reset_repeated(rspamd_log, priv);
			return rspamd_log_file_log(module, id,
									   function,
									   level_flags,
									   message,
									   mlen,
									   rspamd_log,
									   arg);
		}
		else {
			priv->repeats = 0;
		}
	}
	if (!got_time) {
		now = rspamd_get_calendar_ticks();
	}

	gsize niov = rspamd_log_fill_iov(NULL, now, module, id, function, level_flags, message,
									 mlen, rspamd_log);
	struct iovec *iov = g_alloca(sizeof(struct iovec) * niov);
	rspamd_log_fill_iov(iov, now, module, id, function, level_flags, message,
						mlen, rspamd_log);

	return file_log_helper(rspamd_log, priv, iov, niov, level_flags);
}

void *
rspamd_log_file_reload(rspamd_logger_t *logger, struct rspamd_config *cfg,
					   gpointer arg, uid_t uid, gid_t gid, GError **err)
{
	struct rspamd_file_logger_priv *npriv;

	if (!cfg->cfg_name) {
		g_set_error(err, FILE_LOG_QUARK, EINVAL,
					"no log file specified");
		return NULL;
	}

	npriv = rspamd_log_file_init(logger, cfg, uid, gid, err);

	if (npriv) {
		/* Close old */
		rspamd_log_file_dtor(logger, arg);
	}

	return npriv;
}

bool rspamd_log_file_on_fork(rspamd_logger_t *logger, struct rspamd_config *cfg,
							 gpointer arg, GError **err)
{
	struct rspamd_file_logger_priv *priv = (struct rspamd_file_logger_priv *) arg;

	rspamd_log_reset_repeated(logger, priv);
	rspamd_log_flush(logger, priv);

	return true;
}