#include "config.hpp"

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

#include "log.hpp"

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

#include <fcntl.h>
#include <iostream>
#include <string>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <vector>

namespace po = boost::program_options;

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

[[noreturn]] static inline void printHelpAndExit(po::options_description &desc) {
	boost::nowide::cout << desc << std::endl;
	exit(EXIT_FAILURE);
}

[[noreturn]] static inline void printVersionAndExit() {
	boost::nowide::cout << dv::runtime::VERSION_STRING << std::endl;
	exit(EXIT_SUCCESS);
}

void dv::ConfigInit(int argc, char *argv[]) {
	// Allowed command-line options for configuration.
	po::options_description cliDescription("Command-line options");
	cliDescription.add_options()("help,h", "print help text")("version,v", "print runtime version")("ipaddress,i",
		po::value<std::string>(),
		fmt::format("IP address for config-server to listen on (default {})", DEFAULT_ADDRESS).c_str())("port,p",
		po::value<uint16_t>(),
		fmt::format("Port number for config-server to listen on (default {})", DEFAULT_PORT).c_str())("log,l",
		po::value<std::string>(), fmt::format("Path to log file (default '$HOME/{}')", DEFAULT_LOG_FILE).c_str())(
		"systemservice", po::value<bool>(), "Enable system service mode (default OFF)")(
		"console", po::value<bool>(), "Log messages to the console (default ON)")(
		"config,c", po::value<std::string>(), "Use this XML configuration file to initialize the configuration");

	po::variables_map cliVarMap;
	try {
		po::store(boost::program_options::parse_command_line(argc, argv, cliDescription), cliVarMap);
		po::notify(cliVarMap);
	}
	catch (...) {
		boost::nowide::cout << "Failed to parse command-line options!" << std::endl;
		printHelpAndExit(cliDescription);
	}

	// Parse/check command-line options.
	if (cliVarMap.count("help") == 1) {
		printHelpAndExit(cliDescription);
	}

	if (cliVarMap.count("version") == 1) {
		printVersionAndExit();
	}

	{
		bool consoleLogging = true;

		if (cliVarMap.count("console") == 1) {
			consoleLogging = cliVarMap["console"].as<bool>();
		}

		if (!consoleLogging) {
			dv::LoggerInternal::DisableConsoleOutput();
		}
	}

	auto systemNode = dv::Config::GLOBAL.getNode("/system/");

	{
		std::string serverAddress{DEFAULT_ADDRESS};

		if (cliVarMap.count("ipaddress") == 1) {
			serverAddress = cliVarMap["ipaddress"].as<std::string>();
		}

		systemNode.getRelativeNode("server/").create<dv::CfgType::STRING>("ipAddress", serverAddress, {2, 39},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT,
			"IP address where configuration server is listening for client connections.");
	}

	{
		uint16_t serverPort{DEFAULT_PORT};

		if (cliVarMap.count("port") == 1) {
			serverPort = cliVarMap["port"].as<uint16_t>();
		}

		systemNode.getRelativeNode("server/").create<dv::CfgType::INT>("portNumber", serverPort, {1, UINT16_MAX},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT,
			"Port where configuration server is listening for client connections.");
	}

	{
		boost::filesystem::path logFilePath{dv::portable_get_user_home_directory()};
		logFilePath /= DEFAULT_LOG_FILE;

		if (cliVarMap.count("log") == 1) {
			logFilePath = cliVarMap["log"].as<std::string>();
		}

		systemNode.getRelativeNode("logger/").create<dv::CfgType::STRING>("logFile", logFilePath.string(),
			{2, PATH_MAX}, dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT,
			"Path of the file where all log messages are written to.");
	}

	{
		bool systemService = false;

		if (cliVarMap.count("systemservice") == 1) {
			systemService = cliVarMap["systemservice"].as<bool>();
		}

		systemNode.create<dv::CfgType::BOOL>("systemService", systemService, {},
			dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "Runtime is running in system service mode.");
	}

	{
		// Allow user-driven configuration write-back.
		auto configNode = systemNode.getRelativeNode("config/");

		configNode.create<dv::CfgType::STRING>(
			"saveFile", "", {0, PATH_MAX}, dv::CfgFlags::NORMAL, "File path to save the current configuration to.");

		configNode.create<dv::CfgType::BOOL>("writeSaveFile", false, {}, dv::CfgFlags::NORMAL | dv::CfgFlags::NO_EXPORT,
			"Write current configuration to specified XML config file.");
		configNode.attributeModifierButton("writeSaveFile", "Write to file");

		configNode.addAttributeListener(nullptr, &ConfigWriteBackListener);
	}

	// Load initial configuration from file only if requested separately.
	if (cliVarMap.count("config") == 1) {
		// User supplied config file for initialization.
		const boost::filesystem::path initConfigFilePath{cliVarMap["config"].as<std::string>()};

		// Check that config file ends in .xml, exists, and is a normal file.
		if (initConfigFilePath.extension().string() != ".xml") {
			boost::nowide::cout << "Supplied initialization configuration file '" << initConfigFilePath
								<< "' has no XML extension, ignoring ..." << std::endl;
			return;
		}

		auto configNode = systemNode.getRelativeNode("config/");

		// Useful default value if save file is not set.
		if (configNode.getString("saveFile").empty()) {
			configNode.putString("saveFile", initConfigFilePath.lexically_normal().string());
		}

		// Load XML configuration from file.
		if (boost::filesystem::exists(initConfigFilePath) && boost::filesystem::is_regular_file(initConfigFilePath)
			&& (boost::filesystem::file_size(initConfigFilePath) > 0)) {
			if (!dv::Config::GLOBAL.getRootNode().importSubTreeFromXML(initConfigFilePath.string(), true)) {
				boost::nowide::cout << "Supplied initialization configuration file '" << initConfigFilePath
									<< "' could not be imported, ignoring ..." << std::endl;
				return;
			}

			configNode.create<dv::CfgType::STRING>("initFile",
				boost::filesystem::canonical(initConfigFilePath).lexically_normal().string(), {2, PATH_MAX},
				dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT,
				"Path of the file from which the configuration was initialized.");
		}
	}
}

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

	if ((event == DVCFG_ATTRIBUTE_MODIFIED) && (changeType == DVCFG_TYPE_BOOL)
		&& (std::string{changeKey} == "writeSaveFile") && changeValue.boolean) {
		const std::string saveFilePath{dv::Cfg::Node{node}.getString("saveFile")};

		if (!saveFilePath.empty()) {
			// Write out configuration in XML format.
			if (!dv::Config::GLOBAL.getRootNode().exportSubTreeToXML(saveFilePath)) {
				dv::Log(dv::logLevel::ERROR, "Configuration could not be written to specified save file '{:s}'.",
					saveFilePath);
			}
		}
		else {
			dv::Log(dv::logLevel::ERROR, "Configuration save file was not specified.");
		}

		dvConfigNodeAttributeBooleanReset(node, changeKey);
	}
}
