#include "log.hpp"

#include "../include/dv-sdk/cross/portable_io.h"
#include "../include/dv-sdk/cross/portable_time.h"

#include <boost/filesystem.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/nowide/iostream.hpp>

#include <atomic>
#include <iostream>
#include <mutex>
#include <string>

#define MAX_LOG_CONTENT_SIZE 4096

static void removeLinesFromFile(int64_t sizeToSkip);
static void writeMessage(const std::string &logString);
static void logInternal(
	dv::logLevel levelToCompare, dv::logLevel logLevel, std::string_view subSystem, std::string_view message);
static void logShutDownWriteBack(void);
static void logConfigLogger(const char *msg, bool fatal);
static void logLevelListener(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
	const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue);

static constexpr std::string_view LOG_MESSAGE_FORMAT{"{:%Y-%m-%d %H:%M:%S}: {:s}: {:s}: {:s}"};

// atomic variables
static std::atomic<dv::logLevel> systemLogLevel = dv::logLevel::ERROR;

// variables
static struct {
	bool disableConsoleOutput;
	boost::nowide::fstream stream;
	boost::filesystem::path path;
	std::mutex mutex;
} logFile;

static dv::Cfg::Node logNode = nullptr;

// thread variables
static thread_local const dv::LoggerInternal::LogBlock *loggerPtr = nullptr;

void dv::LoggerInternal::Set(const dv::LoggerInternal::LogBlock *_logger) {
	loggerPtr = _logger;
}

const dv::LoggerInternal::LogBlock *dv::LoggerInternal::Get() {
	return (loggerPtr);
}

void dv::LoggerInternal::LogInternal(dv::logLevel level, std::string_view message) {
	auto localLogger = loggerPtr;

	if (localLogger == nullptr) {
		// System default logger.
		logInternal(systemLogLevel.load(std::memory_order_relaxed), level, "Runtime", message);
	}
	else {
		// Specialized logger.
		auto localLogLevel = localLogger->logLevel.load(std::memory_order_relaxed);

		// Only log messages above the specified severity level.
		if (level > localLogLevel) {
			return;
		}

		logInternal(localLogLevel, level, localLogger->logPrefix, message);
	}
}

static void removeLinesFromFile(int64_t sizeToSkip) {
	// lock logFile.mutex already held from call site writeMessage().
	// this function will skip the first sizeToSkip [Bytes] and copy everything to a new file
	// after that will delete the log file and rename the new file as the log file
	boost::filesystem::path tempFilePath{boost::filesystem::temp_directory_path() / boost::filesystem::unique_path()};

	boost::nowide::ofstream tempFile{tempFilePath, std::ios_base::out | std::ios_base::trunc | std::ios_base::binary};

	if (!tempFile.is_open()) {
		return;
	}

	// 10kB of additional size to not resize the file at each new line
	logFile.stream.seekg(sizeToSkip + (10 * 1024));

	// skip first line that is probably cut and make space for the next line
	std::string line;
	std::getline(logFile.stream, line, '\n');

	// Copy rest of content over.
	tempFile << logFile.stream.rdbuf();

	// Close all files.
	tempFile.close();
	logFile.stream.close();

	// Move new file to old log file.
	try {
		boost::filesystem::rename(tempFilePath, logFile.path);
	}
	catch (const boost::filesystem::filesystem_error &) {
		boost::filesystem::copy_file(tempFilePath, logFile.path, boost::filesystem::copy_options::overwrite_existing);
		boost::filesystem::remove(tempFilePath);
	}

	logFile.stream.open(logFile.path,
		std::ios_base::in | std::ios_base::out | std::ios_base::ate | std::ios_base::app | std::ios_base::binary);

	if (!logFile.stream.is_open()) {
		// Must be able to re-open log file! _REQUIRED_
		dv::Log(dv::logLevel::ERROR, "Failed to re-open log file '{:s}'. Error: {:d}.", logFile.path.string(), errno);
		exit(EXIT_FAILURE);
	}

	// Ensure stdout/stderr redirect to file after open.
	if (logFile.disableConsoleOutput) {
		std::cout.rdbuf(logFile.stream.rdbuf());
		std::cerr.rdbuf(logFile.stream.rdbuf());
	}
}

static void writeMessage(const std::string &logString) {
	// Lock to guarantee same order of messages for all outputs.
	std::scoped_lock lock(logFile.mutex);

	// Write to stdout.
	if (!logFile.disableConsoleOutput) {
		boost::nowide::cout << logString << std::endl;
	}

	// Write to file (and resize file if required).
	if (logFile.stream.is_open()) {
		logFile.stream << logString << std::endl;

		const auto maxSize    = logNode.getLong("limitLogSize");
		const auto actualSize = static_cast<std::streamoff>(logFile.stream.tellp());

		if (maxSize < actualSize) {
			removeLinesFromFile(actualSize - maxSize);
		}
	}

	// Write to network.
	if (logNode) {
		logNode.updateReadOnly<dv::CfgType::STRING>("lastLogMessage", logString);
	}
}

static void logInternal(
	dv::logLevel levelToCompare, dv::logLevel logLevel, std::string_view subSystem, std::string_view message) {
	// if subsystem or format are empty log return error
	if (message.empty()) {
		dv::Log(dv::logLevel::ERROR, "Empty message.");
		return;
	}

	// only log if message priority is higher than system log level
	if (logLevel > levelToCompare) {
		return;
	}

	// Time-zone on Windows can contain non UTF-8 charcters, skip it.
	const auto logString = fmt::format(LOG_MESSAGE_FORMAT, dv::localTime(std::chrono::system_clock::now()),
		dv::LoggerInternal::logLevelEnumToName(logLevel), subSystem, message);

	writeMessage(logString);
}

void dv::LoggerInternal::Init() {
	std::int32_t defaultLogSize{20971520};

	logNode = dv::Cfg::Tree::globalTree().getNode("/system/logger/");

	// Ensure default log value is present.
	logNode.create<dv::CfgType::STRING>("logLevel", dv::LoggerInternal::logLevelEnumToName(dv::logLevel::INFO),
		{0, INT32_MAX}, dv::CfgFlags::NORMAL, "Global log-level.");
	logNode.attributeModifierListOptions("logLevel", dv::LoggerInternal::logLevelNamesCommaList(), false);

	// Update log-level to new names.
	const auto &oldLogLevel = logNode.getString("logLevel");

	if ((oldLogLevel == "EMERGENCY") || (oldLogLevel == "ALERT") || (oldLogLevel == "CRITICAL")) {
		logNode.putString("logLevel", "ERROR");
	}
	else if (oldLogLevel == "NOTICE") {
		logNode.putString("logLevel", "INFO");
	}

	logNode.create<dv::CfgType::STRING>("lastLogMessage", "", {0, 32 * 1024},
		dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Path to the file where all log messages are written to.");

	logNode.create<dv::CfgType::LONG>(
		"limitLogSize", defaultLogSize, {0, INT64_MAX}, dv::CfgFlags::NORMAL, "Maximum size for log file in bytes.");

	// Try to open the specified file and error out if not possible.
	{
		std::unique_lock lock(logFile.mutex);

		logFile.path = logNode.get<dv::CfgType::STRING>("logFile");

		logFile.stream.open(logFile.path,
			std::ios_base::in | std::ios_base::out | std::ios_base::ate | std::ios_base::app | std::ios_base::binary);

		if (!logFile.stream.is_open()) {
			// Must be able to open log file! _REQUIRED_
			lock.unlock(); // Avoid lock error in dv::Log() right after.
			dv::Log(dv::logLevel::ERROR, "Failed to open log file '{:s}'. Error: {:d}.", logFile.path.string(), errno);
			exit(EXIT_FAILURE);
		}

		// Ensure stdout/stderr redirect to file after open.
		if (logFile.disableConsoleOutput) {
			std::cout.rdbuf(logFile.stream.rdbuf());
			std::cerr.rdbuf(logFile.stream.rdbuf());
		}
	}

	// Set global log level and install listener for its update.
	auto logLevel = logNode.get<dv::CfgType::STRING>("logLevel");

	systemLogLevel.store(dv::LoggerInternal::logLevelNameToEnum(logLevel));

	logNode.addAttributeListener(nullptr, &logLevelListener);

	// Make sure log file gets flushed at exit time.
	atexit(&logShutDownWriteBack);

	// Now that config is initialized (has to be!) and logging too, we can
	// set the ConfigTree logger to use our internal logger too.
	dvConfigTreeErrorLogCallbackSet(&logConfigLogger);

	// Log sub-system initialized fully and correctly, log this.
	// logFile.path never changes after being set above, safe to not access via lock.
	dv::Log(dv::logLevel::INFO, "Started with log file '{:s}', log-level {:s}.", logFile.path.string(), logLevel);
}

void dv::LoggerInternal::DisableConsoleOutput() {
	std::scoped_lock lock(logFile.mutex);

	logFile.disableConsoleOutput = true;
}

static void logShutDownWriteBack(void) {
	dv::Log(dv::logLevel::DEBUG, "Shutting down, flushing outputs.");

	// Flush interactive outputs.
	std::cout.flush();
	std::cerr.flush();

	// Ensure proper flushing and closing of the log file at shutdown.
	{
		std::scoped_lock lock(logFile.mutex);

		if (logFile.stream.is_open()) {
			logFile.stream.flush();
			logFile.stream.close();
		}
	}
}

static void logConfigLogger(const char *msg, bool fatal) {
	dv::Log(dv::logLevel::ERROR, msg);

	if (fatal) {
		throw std::runtime_error(msg);
	}
}

static void logLevelListener(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
	const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
	UNUSED_ARGUMENT(node);
	UNUSED_ARGUMENT(userData);

	if (event == DVCFG_ATTRIBUTE_MODIFIED && changeType == DVCFG_TYPE_STRING && std::string(changeKey) == "logLevel") {
		// Update the global log level asynchronously.
		systemLogLevel.store(dv::LoggerInternal::logLevelNameToEnum(changeValue.string));

		dv::Log(dv::logLevel::DEBUG, "Log-level set to {:s}.", changeValue.string);
	}
}
