/*
 * Copyright 2025 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 "http_router.h"
#include "http_connection.h"
#include "http_private.h"
#include "libserver/http_content_negotiation.h"
#include "libserver/ssl_util.h"
#include "libutil/regexp.h"
#include "libutil/printf.h"
#include "libserver/logger.h"
#include "utlist.h"
#include "unix-std.h"

enum http_magic_type {
	HTTP_MAGIC_PLAIN = 0,
	HTTP_MAGIC_HTML,
	HTTP_MAGIC_CSS,
	HTTP_MAGIC_JS,
	HTTP_MAGIC_ICO,
	HTTP_MAGIC_PNG,
	HTTP_MAGIC_JPG,
	HTTP_MAGIC_SVG
};

static const struct _rspamd_http_magic {
	const char *ext;
	const char *ct;
} http_file_types[] = {
	[HTTP_MAGIC_PLAIN] = {"txt", "text/plain"},
	[HTTP_MAGIC_HTML] = {"html", "text/html"},
	[HTTP_MAGIC_CSS] = {"css", "text/css"},
	[HTTP_MAGIC_JS] = {"js", "application/javascript"},
	[HTTP_MAGIC_ICO] = {"ico", "image/x-icon"},
	[HTTP_MAGIC_PNG] = {"png", "image/png"},
	[HTTP_MAGIC_JPG] = {"jpg", "image/jpeg"},
	[HTTP_MAGIC_SVG] = {"svg", "image/svg+xml"},
};

/*
 * HTTP router functions
 */

static void
rspamd_http_entry_free(struct rspamd_http_connection_entry *entry)
{
	if (entry != NULL) {
		rspamd_http_connection_unref(entry->conn);
		if (entry->rt->finish_handler) {
			entry->rt->finish_handler(entry);
		}

		DL_DELETE(entry->rt->conns, entry);
		close(entry->conn->fd);
		g_free(entry);
	}
}

static void
rspamd_http_router_error_handler(struct rspamd_http_connection *conn,
								 GError *err)
{
	struct rspamd_http_connection_entry *entry = conn->ud;
	struct rspamd_http_message *msg;

	if (entry->is_reply) {
		/* At this point we need to finish this session and close owned socket */
		if (entry->rt->error_handler != NULL) {
			entry->rt->error_handler(entry, err);
		}
		rspamd_http_entry_free(entry);
	}
	else {
		/* Here we can write a reply to a client */
		if (entry->rt->error_handler != NULL) {
			entry->rt->error_handler(entry, err);
		}
		msg = rspamd_http_new_message(HTTP_RESPONSE);
		msg->date = time(NULL);
		msg->code = err->code;
		rspamd_http_message_set_body(msg, err->message, strlen(err->message));
		rspamd_http_connection_reset(entry->conn);
		rspamd_http_connection_write_message(entry->conn,
											 msg,
											 NULL,
											 "text/plain",
											 entry,
											 entry->rt->timeout);
		entry->is_reply = TRUE;
	}
}

static const char *
rspamd_http_router_detect_ct(const char *path)
{
	const char *dot;
	unsigned int i;

	dot = strrchr(path, '.');
	if (dot == NULL) {
		return http_file_types[HTTP_MAGIC_PLAIN].ct;
	}
	dot++;

	for (i = 0; i < G_N_ELEMENTS(http_file_types); i++) {
		if (strcmp(http_file_types[i].ext, dot) == 0) {
			return http_file_types[i].ct;
		}
	}

	return http_file_types[HTTP_MAGIC_PLAIN].ct;
}

static gboolean
rspamd_http_router_is_subdir(const char *parent, const char *sub)
{
	if (parent == NULL || sub == NULL || *parent == '\0') {
		return FALSE;
	}

	while (*parent != '\0') {
		if (*sub != *parent) {
			return FALSE;
		}
		parent++;
		sub++;
	}

	parent--;
	if (*parent == G_DIR_SEPARATOR) {
		return TRUE;
	}

	return (*sub == G_DIR_SEPARATOR || *sub == '\0');
}

static gboolean
rspamd_http_router_try_file(struct rspamd_http_connection_entry *entry,
							rspamd_ftok_t *lookup, gboolean expand_path)
{
	struct stat st;
	int fd;
	char filebuf[PATH_MAX], realbuf[PATH_MAX], *dir;
	struct rspamd_http_message *reply_msg;

	rspamd_snprintf(filebuf, sizeof(filebuf), "%s%c%T",
					entry->rt->default_fs_path, G_DIR_SEPARATOR, lookup);

	if (realpath(filebuf, realbuf) == NULL ||
		lstat(realbuf, &st) == -1) {
		return FALSE;
	}

	if (S_ISDIR(st.st_mode) && expand_path) {
		/* Try to append 'index.html' to the url */
		rspamd_fstring_t *nlookup;
		rspamd_ftok_t tok;
		gboolean ret;

		nlookup = rspamd_fstring_sized_new(lookup->len + sizeof("index.html"));
		rspamd_printf_fstring(&nlookup, "%T%c%s", lookup, G_DIR_SEPARATOR,
							  "index.html");
		tok.begin = nlookup->str;
		tok.len = nlookup->len;
		ret = rspamd_http_router_try_file(entry, &tok, FALSE);
		rspamd_fstring_free(nlookup);

		return ret;
	}
	else if (!S_ISREG(st.st_mode)) {
		return FALSE;
	}

	/* We also need to ensure that file is inside the defined dir */
	rspamd_strlcpy(filebuf, realbuf, sizeof(filebuf));
	dir = dirname(filebuf);

	if (dir == NULL ||
		!rspamd_http_router_is_subdir(entry->rt->default_fs_path,
									  dir)) {
		return FALSE;
	}

	fd = open(realbuf, O_RDONLY);
	if (fd == -1) {
		return FALSE;
	}

	reply_msg = rspamd_http_new_message(HTTP_RESPONSE);
	reply_msg->date = time(NULL);
	reply_msg->code = 200;
	rspamd_http_router_insert_headers(entry->rt, reply_msg);

	if (!rspamd_http_message_set_body_from_fd(reply_msg, fd)) {
		rspamd_http_message_free(reply_msg);
		close(fd);
		return FALSE;
	}

	close(fd);

	rspamd_http_connection_reset(entry->conn);

	msg_debug("requested file %s", realbuf);
	rspamd_http_connection_write_message(entry->conn, reply_msg, NULL,
										 rspamd_http_router_detect_ct(realbuf), entry,
										 entry->rt->timeout);

	return TRUE;
}

static void
rspamd_http_router_send_error(GError *err,
							  struct rspamd_http_connection_entry *entry)
{
	struct rspamd_http_message *err_msg;

	err_msg = rspamd_http_new_message(HTTP_RESPONSE);
	err_msg->date = time(NULL);
	err_msg->code = err->code;
	rspamd_http_message_set_body(err_msg, err->message,
								 strlen(err->message));
	entry->is_reply = TRUE;
	err_msg->status = rspamd_fstring_new_init(err->message, strlen(err->message));
	rspamd_http_router_insert_headers(entry->rt, err_msg);
	rspamd_http_connection_reset(entry->conn);
	rspamd_http_connection_write_message(entry->conn,
										 err_msg,
										 NULL,
										 "text/plain",
										 entry,
										 entry->rt->timeout);
}


static int
rspamd_http_router_finish_handler(struct rspamd_http_connection *conn,
								  struct rspamd_http_message *msg)
{
	struct rspamd_http_connection_entry *entry = conn->ud;
	rspamd_http_router_handler_t handler = NULL;
	gpointer found;

	GError *err;
	rspamd_ftok_t lookup;
	const rspamd_ftok_t *encoding;
	struct http_parser_url u;
	unsigned int i;
	rspamd_regexp_t *re;
	struct rspamd_http_connection_router *router;
	char *pathbuf = NULL;

	G_STATIC_ASSERT(sizeof(rspamd_http_router_handler_t) ==
					sizeof(gpointer));

	memset(&lookup, 0, sizeof(lookup));
	router = entry->rt;

	if (entry->is_reply) {
		/* Request is finished, it is safe to free a connection */
		rspamd_http_entry_free(entry);
	}
	else {
		if (G_UNLIKELY(msg->method != HTTP_GET && msg->method != HTTP_POST)) {
			if (router->unknown_method_handler) {
				return router->unknown_method_handler(entry, msg);
			}
			else {
				err = g_error_new(HTTP_ERROR, 500,
								  "Invalid method");
				if (entry->rt->error_handler != NULL) {
					entry->rt->error_handler(entry, err);
				}

				rspamd_http_router_send_error(err, entry);
				g_error_free(err);

				return 0;
			}
		}

		/* Search for path */
		if (msg->url != NULL && msg->url->len != 0) {

			http_parser_parse_url(msg->url->str, msg->url->len, TRUE, &u);

			if (u.field_set & (1 << UF_PATH)) {
				gsize unnorm_len;

				pathbuf = g_malloc(u.field_data[UF_PATH].len);
				memcpy(pathbuf, msg->url->str + u.field_data[UF_PATH].off,
					   u.field_data[UF_PATH].len);
				lookup.begin = pathbuf;
				lookup.len = u.field_data[UF_PATH].len;

				rspamd_normalize_path_inplace(pathbuf,
											  lookup.len,
											  &unnorm_len);
				lookup.len = unnorm_len;
			}
			else {
				lookup.begin = msg->url->str;
				lookup.len = msg->url->len;
			}

			found = g_hash_table_lookup(entry->rt->paths, &lookup);
			memcpy(&handler, &found, sizeof(found));
			msg_debug("requested known path: %T", &lookup);
		}
		else {
			err = g_error_new(HTTP_ERROR, 404,
							  "Empty path requested");
			if (entry->rt->error_handler != NULL) {
				entry->rt->error_handler(entry, err);
			}

			rspamd_http_router_send_error(err, entry);
			g_error_free(err);

			return 0;
		}

		entry->is_reply = TRUE;

		encoding = rspamd_http_message_find_header(msg, "Accept-Encoding");

		if (encoding) {
			if (rspamd_substring_search(encoding->begin, encoding->len,
										"gzip", 4) != -1) {
				entry->support_gzip = TRUE;
				entry->compression_flags |= RSPAMD_HTTP_COMPRESS_GZIP;
			}
			if (rspamd_substring_search(encoding->begin, encoding->len,
										"zstd", 4) != -1) {
				entry->compression_flags |= RSPAMD_HTTP_COMPRESS_ZSTD;
			}
			if (rspamd_substring_search(encoding->begin, encoding->len,
										"deflate", 7) != -1) {
				entry->compression_flags |= RSPAMD_HTTP_COMPRESS_DEFLATE;
			}
		}

		if (handler != NULL) {
			if (pathbuf) {
				g_free(pathbuf);
			}

			return handler(entry, msg);
		}
		else {
			/* Try regexps */
			for (i = 0; i < router->regexps->len; i++) {
				re = g_ptr_array_index(router->regexps, i);
				if (rspamd_regexp_match(re, lookup.begin, lookup.len,
										TRUE)) {
					found = rspamd_regexp_get_ud(re);
					memcpy(&handler, &found, sizeof(found));

					if (pathbuf) {
						g_free(pathbuf);
					}

					return handler(entry, msg);
				}
			}

			/* Now try plain file */
			if (entry->rt->default_fs_path == NULL || lookup.len == 0 ||
				!rspamd_http_router_try_file(entry, &lookup, TRUE)) {

				err = g_error_new(HTTP_ERROR, 404,
								  "Not found");
				if (entry->rt->error_handler != NULL) {
					entry->rt->error_handler(entry, err);
				}

				msg_info("path: %T not found", &lookup);
				rspamd_http_router_send_error(err, entry);
				g_error_free(err);
			}
		}
	}

	if (pathbuf) {
		g_free(pathbuf);
	}

	return 0;
}

struct rspamd_http_connection_router *
rspamd_http_router_new(rspamd_http_router_error_handler_t eh,
					   rspamd_http_router_finish_handler_t fh,
					   ev_tstamp timeout,
					   const char *default_fs_path,
					   struct rspamd_http_context *ctx)
{
	struct rspamd_http_connection_router *nrouter;
	struct stat st;

	nrouter = g_malloc0(sizeof(struct rspamd_http_connection_router));
	nrouter->paths = g_hash_table_new_full(rspamd_ftok_icase_hash,
										   rspamd_ftok_icase_equal, rspamd_fstring_mapped_ftok_free, NULL);
	nrouter->regexps = g_ptr_array_new();
	nrouter->conns = NULL;
	nrouter->error_handler = eh;
	nrouter->finish_handler = fh;
	nrouter->response_headers = g_hash_table_new_full(rspamd_strcase_hash,
													  rspamd_strcase_equal, g_free, g_free);
	nrouter->event_loop = ctx->event_loop;
	nrouter->timeout = timeout;
	nrouter->default_fs_path = NULL;

	if (default_fs_path != NULL) {
		if (stat(default_fs_path, &st) == -1) {
			msg_err("cannot stat %s", default_fs_path);
		}
		else {
			if (!S_ISDIR(st.st_mode)) {
				msg_err("path %s is not a directory", default_fs_path);
			}
			else {
				nrouter->default_fs_path = realpath(default_fs_path, NULL);
			}
		}
	}

	nrouter->ctx = ctx;

	return nrouter;
}

void rspamd_http_router_set_key(struct rspamd_http_connection_router *router,
								struct rspamd_cryptobox_keypair *key)
{
	g_assert(key != NULL);

	router->key = rspamd_keypair_ref(key);
}

void rspamd_http_router_add_path(struct rspamd_http_connection_router *router,
								 const char *path, rspamd_http_router_handler_t handler)
{
	gpointer ptr;
	rspamd_ftok_t *key;
	rspamd_fstring_t *storage;
	G_STATIC_ASSERT(sizeof(rspamd_http_router_handler_t) ==
					sizeof(gpointer));

	if (path != NULL && handler != NULL && router != NULL) {
		memcpy(&ptr, &handler, sizeof(ptr));
		storage = rspamd_fstring_new_init(path, strlen(path));
		key = g_malloc0(sizeof(*key));
		key->begin = storage->str;
		key->len = storage->len;
		g_hash_table_insert(router->paths, key, ptr);
	}
}

void rspamd_http_router_set_unknown_handler(struct rspamd_http_connection_router *router,
											rspamd_http_router_handler_t handler)
{
	if (router != NULL) {
		router->unknown_method_handler = handler;
	}
}

void rspamd_http_router_add_header(struct rspamd_http_connection_router *router,
								   const char *name, const char *value)
{
	if (name != NULL && value != NULL && router != NULL) {
		g_hash_table_replace(router->response_headers, g_strdup(name),
							 g_strdup(value));
	}
}

void rspamd_http_router_insert_headers(struct rspamd_http_connection_router *router,
									   struct rspamd_http_message *msg)
{
	GHashTableIter it;
	gpointer k, v;

	if (router && msg) {
		g_hash_table_iter_init(&it, router->response_headers);

		while (g_hash_table_iter_next(&it, &k, &v)) {
			rspamd_http_message_add_header(msg, k, v);
		}
	}
}

void rspamd_http_router_add_regexp(struct rspamd_http_connection_router *router,
								   struct rspamd_regexp_s *re, rspamd_http_router_handler_t handler)
{
	gpointer ptr;
	G_STATIC_ASSERT(sizeof(rspamd_http_router_handler_t) ==
					sizeof(gpointer));

	if (re != NULL && handler != NULL && router != NULL) {
		memcpy(&ptr, &handler, sizeof(ptr));
		rspamd_regexp_set_ud(re, ptr);
		g_ptr_array_add(router->regexps, rspamd_regexp_ref(re));
	}
}

void rspamd_http_router_set_ssl(struct rspamd_http_connection_router *router,
								gpointer ssl_ctx)
{
	g_assert(router != NULL);
	router->server_ssl_ctx = ssl_ctx;
}

void rspamd_http_router_set_max_size(struct rspamd_http_connection_router *router,
									 gsize sz)
{
	g_assert(router != NULL);
	router->max_size = sz;
}

void rspamd_http_router_handle_socket_ssl(struct rspamd_http_connection_router *router,
										  int fd, gpointer ud, gboolean ssl)
{
	struct rspamd_http_connection_entry *conn;

	conn = g_malloc0(sizeof(struct rspamd_http_connection_entry));
	conn->rt = router;
	conn->ud = ud;
	conn->is_reply = FALSE;

	conn->conn = rspamd_http_connection_new_server(router->ctx,
												   fd,
												   NULL,
												   rspamd_http_router_error_handler,
												   rspamd_http_router_finish_handler,
												   0);

	rspamd_http_connection_set_max_size(conn->conn, router->max_size);

	if (router->key) {
		rspamd_http_connection_set_key(conn->conn, router->key);
	}

	if (ssl && router->server_ssl_ctx) {
		rspamd_http_connection_accept_ssl(conn->conn, router->server_ssl_ctx,
										  conn, router->timeout);
	}
	else {
		rspamd_http_connection_read_message(conn->conn, conn, router->timeout);
	}

	DL_PREPEND(router->conns, conn);
}

void rspamd_http_router_handle_socket(struct rspamd_http_connection_router *router,
									  int fd, gpointer ud)
{
	rspamd_http_router_handle_socket_ssl(router, fd, ud, FALSE);
}

void rspamd_http_router_free(struct rspamd_http_connection_router *router)
{
	struct rspamd_http_connection_entry *conn, *tmp;
	rspamd_regexp_t *re;
	unsigned int i;

	if (router) {
		DL_FOREACH_SAFE(router->conns, conn, tmp)
		{
			rspamd_http_entry_free(conn);
		}

		if (router->key) {
			rspamd_keypair_unref(router->key);
		}

		if (router->server_ssl_ctx) {
			rspamd_ssl_ctx_free(router->server_ssl_ctx);
		}

		if (router->default_fs_path != NULL) {
			g_free(router->default_fs_path);
		}

		for (i = 0; i < router->regexps->len; i++) {
			re = g_ptr_array_index(router->regexps, i);
			rspamd_regexp_unref(re);
		}

		g_ptr_array_free(router->regexps, TRUE);
		g_hash_table_unref(router->paths);
		g_hash_table_unref(router->response_headers);
		g_free(router);
	}
}
