/*
 * 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 "file_util.hxx"
#include "contrib/fmt/include/fmt/core.h"
#include "libutil/util.h"
#include "libutil/unix-std.h"

#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL

#include "doctest/doctest.h"

namespace rspamd::util {

auto raii_file::open(const char *fname, int flags) -> tl::expected<raii_file, error>
{
	int oflags = flags;
#ifdef O_CLOEXEC
	oflags |= O_CLOEXEC;
#endif

	if (fname == nullptr) {
		return tl::make_unexpected(error{"cannot open file; filename is nullptr", EINVAL, error_category::CRITICAL});
	}

	auto fd = ::open(fname, oflags);

	if (fd == -1) {
		return tl::make_unexpected(error{fmt::format("cannot open file {}: {}", fname, ::strerror(errno)), errno});
	}

	auto ret = raii_file{fname, fd, false};

	if (fstat(ret.fd, &ret.st) == -1) {
		return tl::make_unexpected(error{fmt::format("cannot stat file {}: {}", fname, ::strerror(errno)), errno});
	}

	return ret;
}

auto raii_file::create(const char *fname, int flags, int perms) -> tl::expected<raii_file, error>
{
	int oflags = flags | O_CREAT;
#ifdef O_CLOEXEC
	oflags |= O_CLOEXEC;
#endif

	if (fname == nullptr) {
		return tl::make_unexpected(error{"cannot create file; filename is nullptr", EINVAL, error_category::CRITICAL});
	}

	auto fd = ::open(fname, oflags, perms);

	if (fd == -1) {
		return tl::make_unexpected(error{fmt::format("cannot create file {}: {}", fname, ::strerror(errno)), errno});
	}

	auto ret = raii_file{fname, fd, false};

	if (fstat(ret.fd, &ret.st) == -1) {
		return tl::make_unexpected(error{fmt::format("cannot stat file {}: {}", fname, ::strerror(errno)), errno});
	}

	return ret;
}

auto raii_file::create_temp(const char *fname, int flags, int perms) -> tl::expected<raii_file, error>
{
	int oflags = flags;
#ifdef O_CLOEXEC
	oflags |= O_CLOEXEC | O_CREAT | O_EXCL;
#endif
	if (fname == nullptr) {
		return tl::make_unexpected(error{"cannot open file; filename is nullptr", EINVAL, error_category::CRITICAL});
	}

	auto fd = ::open(fname, oflags, perms);

	if (fd == -1) {
		return tl::make_unexpected(error{fmt::format("cannot create file {}: {}", fname, ::strerror(errno)), errno});
	}

	auto ret = raii_file{fname, fd, true};

	if (fstat(ret.fd, &ret.st) == -1) {
		return tl::make_unexpected(error{fmt::format("cannot stat file {}: {}", fname, ::strerror(errno)), errno});
	}

	return ret;
}

auto raii_file::mkstemp(const char *pattern, int flags, int perms) -> tl::expected<raii_file, error>
{
	int oflags = flags;
#ifdef O_CLOEXEC
	oflags |= O_CLOEXEC | O_CREAT | O_EXCL;
#endif
	if (pattern == nullptr) {
		return tl::make_unexpected(error{"cannot open file; pattern is nullptr", EINVAL, error_category::CRITICAL});
	}

	std::string mutable_pattern = pattern;

	auto fd = g_mkstemp_full(mutable_pattern.data(), oflags, perms);

	if (fd == -1) {
		return tl::make_unexpected(error{fmt::format("cannot create file {}: {}", pattern, ::strerror(errno)), errno});
	}

	auto ret = raii_file{mutable_pattern.c_str(), fd, true};

	if (fstat(ret.fd, &ret.st) == -1) {
		return tl::make_unexpected(error{fmt::format("cannot stat file {}: {}",
													 mutable_pattern, ::strerror(errno)),
										 errno});
	}

	return ret;
}

raii_file::~raii_file() noexcept
{
	if (fd != -1) {
		if (temp) {
			(void) unlink(fname.c_str());
		}
		close(fd);
	}
}

auto raii_file::update_stat() noexcept -> bool
{
	return fstat(fd, &st) != -1;
}

raii_file::raii_file(const char *fname, int fd, bool temp)
	: fd(fd), temp(temp)
{
	std::size_t nsz;

	/* Normalize path */
	this->fname = fname;
	rspamd_normalize_path_inplace(this->fname.data(), this->fname.size(), &nsz);
	this->fname.resize(nsz);
}


raii_locked_file::~raii_locked_file() noexcept
{
	if (fd != -1) {
		(void) rspamd_file_unlock(fd, FALSE);
	}
}

auto raii_locked_file::lock_raii_file(raii_file &&unlocked) -> tl::expected<raii_locked_file, error>
{
	if (!rspamd_file_lock(unlocked.get_fd(), TRUE)) {
		return tl::make_unexpected(
			error{fmt::format("cannot lock file {}: {}", unlocked.get_name(), ::strerror(errno)), errno});
	}

	return raii_locked_file{std::move(unlocked)};
}

auto raii_locked_file::unlock() -> raii_file
{
	if (fd != -1) {
		(void) rspamd_file_unlock(fd, FALSE);
	}

	return raii_file{static_cast<raii_file &&>(std::move(*this))};
}

raii_mmaped_file::raii_mmaped_file(raii_file &&file, void *map, std::size_t sz)
	: file(std::move(file)), map(map), map_size(sz)
{
}

auto raii_mmaped_file::mmap_shared(raii_file &&file,
								   int flags, std::int64_t offset) -> tl::expected<raii_mmaped_file, error>
{
	void *map;

	if (file.get_stat().st_size < offset || offset < 0) {
		return tl::make_unexpected(error{
			fmt::format("cannot mmap file {} due to incorrect offset; offset={}, size={}",
						file.get_name(), offset, file.get_size()),
			EINVAL});
	}
	/* Update stat on file to ensure it is up-to-date */
	file.update_stat();
	map = mmap(nullptr, (std::size_t)(file.get_size() - offset), flags, MAP_SHARED, file.get_fd(), offset);

	if (map == MAP_FAILED) {
		return tl::make_unexpected(error{fmt::format("cannot mmap file {}: {}",
													 file.get_name(), ::strerror(errno)),
										 errno});
	}

	return raii_mmaped_file{std::move(file), map, (std::size_t)(file.get_size() - offset)};
}

auto raii_mmaped_file::mmap_shared(const char *fname, int open_flags,
								   int mmap_flags, std::int64_t offset) -> tl::expected<raii_mmaped_file, error>
{
	auto file = raii_file::open(fname, open_flags);

	if (!file.has_value()) {
		return tl::make_unexpected(file.error());
	}

	return raii_mmaped_file::mmap_shared(std::move(file.value()), mmap_flags, offset);
}

raii_mmaped_file::~raii_mmaped_file()
{
	if (map != nullptr) {
		munmap(map, map_size);
	}
}

raii_mmaped_file::raii_mmaped_file(raii_mmaped_file &&other) noexcept
	: file(std::move(other.file))
{
	std::swap(map, other.map);
	std::swap(map_size, other.map_size);
}

auto raii_file_sink::create(const char *fname, int flags, int perms,
							const char *suffix) -> tl::expected<raii_file_sink, error>
{
	if (!fname || !suffix) {
		return tl::make_unexpected(error{"cannot create file; filename is nullptr", EINVAL, error_category::CRITICAL});
	}

	auto tmp_fname = fmt::format("{}.{}", fname, suffix);
	auto file = raii_locked_file::create(tmp_fname.c_str(), flags, perms);

	if (!file.has_value()) {
		return tl::make_unexpected(file.error());
	}

	return raii_file_sink{std::move(file.value()), fname, std::move(tmp_fname)};
}

auto raii_file_sink::write_output() -> bool
{
	if (success) {
		/* We cannot write output twice */
		return false;
	}

	if (rename(tmp_fname.c_str(), output_fname.c_str()) == -1) {
		return false;
	}

	success = true;

	return true;
}

raii_file_sink::~raii_file_sink()
{
	if (!success) {
		/* Unlink sink */
		unlink(tmp_fname.c_str());
	}
}

raii_file_sink::raii_file_sink(raii_locked_file &&_file, const char *_output, std::string &&_tmp_fname)
	: file(std::move(_file)), output_fname(_output), tmp_fname(std::move(_tmp_fname)), success(false)
{
}

raii_file_sink::raii_file_sink(raii_file_sink &&other) noexcept
	: file(std::move(other.file)),
	  output_fname(std::move(other.output_fname)),
	  tmp_fname(std::move(other.tmp_fname)),
	  success(other.success)
{
}

namespace tests {
template<class T>
static auto test_read_file(const T &f)
{
	auto fd = f.get_fd();
	(void) ::lseek(fd, 0, SEEK_SET);
	std::string buf('\0', (std::size_t) f.get_size());
	::read(fd, buf.data(), buf.size());
	return buf;
}
template<class T>
static auto test_write_file(const T &f, const std::string_view &buf)
{
	auto fd = f.get_fd();
	(void) ::lseek(fd, 0, SEEK_SET);
	return ::write(fd, buf.data(), buf.size());
}
auto random_fname(std::string_view extension)
{
	const auto *tmpdir = getenv("TMPDIR");
	if (tmpdir == nullptr) {
		tmpdir = G_DIR_SEPARATOR_S "tmp";
	}

	std::string out_fname{tmpdir};
	out_fname += G_DIR_SEPARATOR_S;

	char hexbuf[32];
	rspamd_random_hex(hexbuf, sizeof(hexbuf));
	out_fname.append((const char *) hexbuf, sizeof(hexbuf));
	if (!extension.empty()) {
		out_fname.append(".");
		out_fname.append(extension);
	}

	return out_fname;
}
TEST_SUITE("loked files utils")
{

	TEST_CASE("create and delete file")
	{
		auto fname = random_fname("tmp");
		{
			auto raii_locked_file = raii_locked_file::create_temp(fname.c_str(), O_RDONLY, 00600);
			CHECK(raii_locked_file.has_value());
			CHECK(raii_locked_file.value().get_extension() == "tmp");
			CHECK(::access(fname.c_str(), R_OK) == 0);
		}
		// File must be deleted after this call
		auto ret = ::access(fname.c_str(), R_OK);
		auto serrno = errno;
		CHECK(ret == -1);
		CHECK(serrno == ENOENT);
		// Create one more time
		{
			auto raii_locked_file = raii_locked_file::create_temp(fname.c_str(), O_RDONLY, 00600);
			CHECK(raii_locked_file.has_value());
			CHECK(::access(fname.c_str(), R_OK) == 0);
		}
		ret = ::access(fname.c_str(), R_OK);
		serrno = errno;
		CHECK(ret == -1);
		CHECK(serrno == ENOENT);
	}

	TEST_CASE("check lock")
	{
		auto fname = random_fname("");
		{
			auto raii_locked_file = raii_locked_file::create_temp(fname.c_str(), O_RDONLY, 00600);
			CHECK(raii_locked_file.has_value());
			CHECK(raii_locked_file.value().get_extension() == "");
			CHECK(::access(fname.c_str(), R_OK) == 0);
			auto raii_locked_file2 = raii_locked_file::open(fname.c_str(), O_RDONLY);
			CHECK(!raii_locked_file2.has_value());
			CHECK(::access(fname.c_str(), R_OK) == 0);
		}
		// File must be deleted after this call
		auto ret = ::access(fname.c_str(), R_OK);
		auto serrno = errno;
		CHECK(ret == -1);
		CHECK(serrno == ENOENT);
	}

	auto get_tmpdir()->std::string
	{
		const auto *tmpdir = getenv("TMPDIR");
		if (tmpdir == nullptr) {
			tmpdir = G_DIR_SEPARATOR_S "tmp";
		}

		std::size_t sz;
		std::string mut_fname = tmpdir;
		rspamd_normalize_path_inplace(mut_fname.data(), mut_fname.size(), &sz);
		mut_fname.resize(sz);

		if (!mut_fname.ends_with(G_DIR_SEPARATOR)) {
			mut_fname += G_DIR_SEPARATOR;
		}

		return mut_fname;
	}

	TEST_CASE("tempfile")
	{
		std::string tmpname;
		const std::string tmpdir{get_tmpdir()};
		{
			auto raii_locked_file = raii_locked_file::mkstemp(std::string(tmpdir + G_DIR_SEPARATOR_S + "doctest-XXXXXXXX").c_str(),
															  O_RDONLY, 00600);
			CHECK(raii_locked_file.has_value());
			CHECK(raii_locked_file.value().get_dir() == tmpdir);
			CHECK(access(raii_locked_file.value().get_name().data(), R_OK) == 0);
			auto raii_locked_file2 = raii_locked_file::open(raii_locked_file.value().get_name().data(), O_RDONLY);
			CHECK(!raii_locked_file2.has_value());
			CHECK(access(raii_locked_file.value().get_name().data(), R_OK) == 0);
			tmpname = raii_locked_file.value().get_name();
		}
		// File must be deleted after this call
		auto ret = ::access(tmpname.c_str(), R_OK);
		auto serrno = errno;
		CHECK(ret == -1);
		CHECK(serrno == ENOENT);
	}

	TEST_CASE("mmap")
	{
		std::string tmpname;
		const std::string tmpdir{get_tmpdir()};
		{
			auto raii_file = raii_file::mkstemp(std::string(tmpdir + G_DIR_SEPARATOR_S + "doctest-XXXXXXXX").c_str(),
												O_RDWR | O_CREAT | O_EXCL, 00600);
			CHECK(raii_file.has_value());
			CHECK(raii_file->get_dir() == tmpdir);
			CHECK(access(raii_file->get_name().data(), R_OK) == 0);
			tmpname = std::string{raii_file->get_name()};
			char payload[] = {'1', '2', '3'};
			CHECK(write(raii_file->get_fd(), payload, sizeof(payload)) == sizeof(payload));
			auto mmapped_file1 = raii_mmaped_file::mmap_shared(std::move(raii_file.value()), PROT_READ | PROT_WRITE);
			CHECK(mmapped_file1.has_value());
			CHECK(!raii_file->is_valid());
			CHECK(mmapped_file1->get_size() == sizeof(payload));
			CHECK(memcmp(mmapped_file1->get_map(), payload, sizeof(payload)) == 0);
			*(char *) mmapped_file1->get_map() = '2';
			auto mmapped_file2 = raii_mmaped_file::mmap_shared(tmpname.c_str(), O_RDONLY, PROT_READ);
			CHECK(mmapped_file2.has_value());
			CHECK(mmapped_file2->get_size() == sizeof(payload));
			CHECK(memcmp(mmapped_file2->get_map(), payload, sizeof(payload)) != 0);
			CHECK(memcmp(mmapped_file2->get_map(), mmapped_file1->get_map(), sizeof(payload)) == 0);
		}
		// File must be deleted after this call
		auto ret = ::access(tmpname.c_str(), R_OK);
		auto serrno = errno;
		CHECK(ret == -1);
		CHECK(serrno == ENOENT);
	}

}// TEST_SUITE

}// namespace tests

}// namespace rspamd::util
