#include "config_server.h"

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

#include <boost/version.hpp>

#include <algorithm>
#include <string>
#include <system_error>

#define DV_CONFIG_SERVER_NAME "ConfigServer"

// 0 is default system ID.
thread_local uint64_t ConfigServer::currentClientID{0};

ConfigServer::ConfigServer() :
	ioThreadRun(true),
	ioThreadState(IOThreadState::STOPPED),
	acceptor(ioService),
	tlsContext(asioSSL::context::tlsv12_server),
	tlsEnabled(false),
	numPushClients(0) {
	// Get the right configuration node first.
	auto serverNode = dv::Cfg::GLOBAL.getNode("/system/server/");

	// Support restarting the config server.
	serverNode.create<dv::CfgType::BOOL>("restart", false, {}, dv::CfgFlags::NORMAL | dv::CfgFlags::NO_EXPORT,
		"Restart configuration server, disconnects all clients and reloads itself.");
	serverNode.attributeModifierButton("restart", "Restart");
	serverNode.addAttributeListener(this, &ConfigServer::restartListener);

	serverNode.create<dv::CfgType::STRING>("logLevel", dv::LoggerInternal::logLevelEnumToName(dv::logLevel::INFO),
		{0, INT32_MAX}, dv::CfgFlags::NORMAL, "Configuration server log-level.");
	serverNode.attributeModifierListOptions("logLevel", dv::LoggerInternal::logLevelNamesCommaList(), false);

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

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

	serverNode.addAttributeListener(&logger.logLevel, &ConfigServer::logLevelListener);

	logger.logLevel.store(dv::LoggerInternal::logLevelNameToEnum(serverNode.get<dv::CfgType::STRING>("logLevel")));
	logger.logPrefix = DV_CONFIG_SERVER_NAME;

	// Default values for TLS secure connection support.
	serverNode.create<dv::CfgType::BOOL>(
		"tls", false, {}, dv::CfgFlags::NORMAL, "Require TLS encryption for configuration server communication.");
	serverNode.create<dv::CfgType::STRING>(
		"tlsCertFile", "", {0, PATH_MAX}, dv::CfgFlags::NORMAL, "Path to TLS certificate file (PEM format).");
	serverNode.create<dv::CfgType::STRING>(
		"tlsKeyFile", "", {0, PATH_MAX}, dv::CfgFlags::NORMAL, "Path to TLS private key file (PEM format).");

	serverNode.create<dv::CfgType::BOOL>(
		"tlsClientVerification", false, {}, dv::CfgFlags::NORMAL, "Require TLS client certificate verification.");
	serverNode.create<dv::CfgType::STRING>("tlsClientVerificationFile", "", {0, PATH_MAX}, dv::CfgFlags::NORMAL,
		"Path to TLS CA file for client verification (PEM format). Leave empty to use system defaults.");
}

ConfigServer::~ConfigServer() {
	auto serverNode = dv::Cfg::GLOBAL.getNode("/system/server/");

	// Remove restart listener first.
	serverNode.removeAttributeListener(this, &ConfigServer::restartListener);
	serverNode.removeAttributeListener(&logger.logLevel, &ConfigServer::logLevelListener);
}

void ConfigServer::threadStart() {
	ioThread = std::thread([this]() {
		// Set thread name.
		portable_thread_set_name(DV_CONFIG_SERVER_NAME);
		portable_thread_set_priority_highest();

		// Setup logger.
		dv::LoggerInternal::Set(&logger);

		while (ioThreadRun) {
			ioThreadState = IOThreadState::STARTING;

			// Start server.
			serviceStart();

			ioThreadState = IOThreadState::STOPPED;

			if (ioThreadRun) {
				// Ready for new run.
#if BOOST_ASIO_NET_NEW_INTERFACE == 1
				ioService.restart();
#else
				ioService.reset();
#endif

				// Configure server.
				try {
					serviceConfigure();
				}
				catch (const boost::system::system_error &) {
					// On failure here, shutdown the whole runtime, as we cannot
					// control it anymore with the given network configuration.
					ioThreadRun = false;
					dv::Cfg::GLOBAL.getNode("/system/").putBool("running", false);
				}
			}
		}
	});
}

void ConfigServer::serviceRestart() {
	if (!ioService.stopped()) {
		asio::post(ioService, [this]() {
			serviceStop();
		});
	}
}

void ConfigServer::threadStop() {
	if (!ioService.stopped()) {
		asio::post(ioService, [this]() {
			ioThreadRun = false;
			serviceStop();
		});
	}

	ioThread.join();
}

void ConfigServer::setCurrentClientID(uint64_t clientID) {
	currentClientID = clientID;
}

uint64_t ConfigServer::getCurrentClientID() {
	return (currentClientID);
}

void ConfigServer::removeClient(ConfigServerConnection *client) {
	removePushClient(client);

	dv::vectorRemove(clients, client);
}

void ConfigServer::addPushClient(ConfigServerConnection *pushClient) {
	numPushClients++;
	pushClients.push_back(pushClient);
}

void ConfigServer::removePushClient(ConfigServerConnection *pushClient) {
	if (dv::vectorRemove(pushClients, pushClient)) {
		numPushClients--;
	}
}

bool ConfigServer::pushClientsPresent() {
	return (!ioService.stopped() && (numPushClients > 0));
}

void ConfigServer::pushMessageToClients(std::shared_ptr<const flatbuffers::FlatBufferBuilder> message) {
	if (pushClientsPresent()) {
		asio::post(ioService, [this, message]() {
			for (auto client : pushClients) {
				client->writePushMessage(message);
			}
		});
	}
}

void ConfigServer::serviceConfigure() {
	// Get config.
	auto serverNode = dv::Cfg::GLOBAL.getNode("/system/server/");

	// Configure acceptor.
	auto ipAddress  = serverNode.get<dv::CfgType::STRING>("ipAddress");
	auto portNumber = serverNode.get<dv::CfgType::INT>("portNumber");

	auto endpoint = asioTCP::endpoint(asioIP::make_address(ipAddress), static_cast<uint16_t>(portNumber));

	// This can fail if port already in use.
	// There's nothing we can do really, stop.
	try {
		acceptor.open(endpoint.protocol());
		acceptor.set_option(asioTCP::socket::reuse_address(true));
		acceptor.bind(endpoint);
		acceptor.listen();
	}
	catch (const boost::system::system_error &ex) {
		dv::Log(dv::logLevel::ERROR,
			"Failed to start server. Error: {:s}. Another dv-runtime might already be running.", ex.what());
		throw;
	}

	// Configure TLS support.
	tlsEnabled = serverNode.get<dv::CfgType::BOOL>("tls");

	if (tlsEnabled) {
		try {
			tlsContext.use_certificate_chain_file(serverNode.get<dv::CfgType::STRING>("tlsCertFile"));
		}
		catch (const boost::system::system_error &ex) {
			dv::Log(dv::logLevel::ERROR, "Failed to load certificate file (error '{:s}'), disabling TLS.", ex.what());
			throw;
		}

		try {
			tlsContext.use_private_key_file(serverNode.get<dv::CfgType::STRING>("tlsKeyFile"), asioSSL::context::pem);
		}
		catch (const boost::system::system_error &ex) {
			dv::Log(dv::logLevel::ERROR, "Failed to load private key file (error '{:s}'), disabling TLS.", ex.what());
			throw;
		}

		tlsContext.set_options(asioSSL::context::default_workarounds | asioSSL::context::single_dh_use);

		// Default: no client verification enforced.
		tlsContext.set_verify_mode(asioSSL::context::verify_peer);

		if (serverNode.get<dv::CfgType::BOOL>("tlsClientVerification")) {
			const std::string tlsVerifyFile = serverNode.get<dv::CfgType::STRING>("tlsClientVerificationFile");

			if (tlsVerifyFile.empty()) {
				tlsContext.set_default_verify_paths();
			}
			else {
				try {
					tlsContext.load_verify_file(tlsVerifyFile);
				}
				catch (const boost::system::system_error &ex) {
					dv::Log(dv::logLevel::ERROR,
						"Failed to load certificate authority verification file (error '{:s}') for client "
						"verification, disabling TLS.",
						ex.what());
					throw;
				}
			}

			tlsContext.set_verify_mode(asioSSL::context::verify_peer | asioSSL::context::verify_fail_if_no_peer_cert);
		}
	}

	// If port was zero, we want to update with the actual port number.
	if (serverNode.get<dv::CfgType::INT>("portNumber") == 0) {
		serverNode.updateReadOnlyAttribute<dv::CfgType::INT>("portNumber", acceptor.local_endpoint().port());
	}

	dv::Log(dv::logLevel::INFO, "Configuration server service ready on {:s}:{:d}.",
		serverNode.get<dv::CfgType::STRING>("ipAddress"), serverNode.get<dv::CfgType::INT>("portNumber"));
}

void ConfigServer::serviceStart() {
	dv::Log(dv::logLevel::DEBUG, "Starting configuration server service.");

	// Start accepting connections.
	acceptStart();

	dv::Cfg::GLOBAL.globalNodeListenerSet(&ConfigServer::globalNodeChangeListener, this);
	dv::Cfg::GLOBAL.globalAttributeListenerSet(&ConfigServer::globalAttributeChangeListener, this);

	// Run IO service.
	ioThreadState = IOThreadState::RUNNING;
	ioService.run();
}

void ConfigServer::serviceStop() {
	// Prevent multiple calls.
	if (ioThreadState != IOThreadState::RUNNING) {
		return;
	}

	ioThreadState = IOThreadState::STOPPING;

	dv::Cfg::GLOBAL.globalAttributeListenerSet(nullptr, nullptr);
	dv::Cfg::GLOBAL.globalNodeListenerSet(nullptr, nullptr);

	// Stop accepting connections.
	acceptor.close();

	// Post 'close all connections' to end of async queue,
	// so that any other callbacks, such as pending accepts,
	// are executed first, and we really close all sockets.
	asio::post(ioService, [this]() {
		// Close all open connections, hard.
		for (const auto client : clients) {
			client->close();
		}
	});

	dv::Log(dv::logLevel::DEBUG, "Stopping configuration server service.");
}

void ConfigServer::acceptStart() {
	acceptor.async_accept([this](const boost::system::error_code &error, asioTCP::socket peer) {
		if (error) {
			// Ignore cancel error, normal on shutdown.
			if (error != asio::error::operation_aborted) {
				dv::Log(dv::logLevel::ERROR, "Failed to accept new connection. Error: {:s} ({:d}).", error.message(),
					error.value());
			}
		}
		else {
			auto client = std::make_shared<ConfigServerConnection>(std::move(peer), tlsEnabled, &tlsContext, this);

			clients.push_back(client.get());

			client->start();

			acceptStart();
		}
	});
}

void ConfigServer::restartListener(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
	const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
	auto server = static_cast<ConfigServer *>(userData);

	if ((event == DVCFG_ATTRIBUTE_MODIFIED) && (changeType == DVCFG_TYPE_BOOL) && (std::string{changeKey} == "restart")
		&& changeValue.boolean) {
		server->serviceRestart();

		dvConfigNodeAttributeBooleanReset(node, changeKey);
	}
}

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

	auto logLevel = static_cast<std::atomic<dv::logLevel> *>(userData);

	if (event == DVCFG_ATTRIBUTE_MODIFIED && changeType == DVCFG_TYPE_STRING && std::string(changeKey) == "logLevel") {
		logLevel->store(dv::LoggerInternal::logLevelNameToEnum(changeValue.string));
	}
}

void ConfigServer::globalNodeChangeListener(
	dvConfigNode n, void *userData, enum dvConfigNodeEvents event, const char *changeNode) {
	auto server = static_cast<ConfigServer *>(userData);
	dv::Cfg::Node node(n);

	if (server->pushClientsPresent()) {
		auto msgBuild = std::make_shared<flatbuffers::FlatBufferBuilder>(DV_CONFIG_SERVER_MAX_INCOMING_SIZE);

		std::string nodePath(node.getPath());
		nodePath += changeNode;
		nodePath.push_back('/');

		auto nodeStr = msgBuild->CreateString(nodePath);

		dv::ConfigActionDataBuilder msg(*msgBuild);

		// Set message ID to the ID of the client that originated this change.
		// If we're running in any other thread it will be 0 (system), if the
		// change we're pushing comes from a listener firing in response to
		// changes brought by a client via the config-server, the current
		// client ID will be the one from that remote client.
		msg.add_id(server->getCurrentClientID());

		msg.add_action(dv::ConfigAction::PUSH_MESSAGE_NODE);
		msg.add_nodeEvents(static_cast<dv::ConfigNodeEvents>(event));
		msg.add_node(nodeStr);

		// Finish off message.
		auto msgRoot = msg.Finish();

		// Write root node and message size.
		dv::FinishSizePrefixedConfigActionDataBuffer(*msgBuild, msgRoot);

		server->pushMessageToClients(msgBuild);
	}
}

void ConfigServer::globalAttributeChangeListener(dvConfigNode n, void *userData, enum dvConfigAttributeEvents event,
	const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
	auto server = static_cast<ConfigServer *>(userData);
	dv::Cfg::Node node(n);

	if (server->pushClientsPresent()) {
		auto msgBuild = std::make_shared<flatbuffers::FlatBufferBuilder>(DV_CONFIG_SERVER_MAX_INCOMING_SIZE);

		auto type  = static_cast<dv::Cfg::AttributeType>(changeType);
		auto flags = node.getAttributeFlags(changeKey, type);

		const std::string valueStr = dv::Cfg::Helper::valueToStringConverter(type, changeValue);

		auto nodeStr = msgBuild->CreateString(node.getPath());
		auto keyStr  = msgBuild->CreateString(changeKey);
		auto valStr  = msgBuild->CreateString(valueStr);

		const std::string rangesStr
			= dv::Cfg::Helper::rangesToStringConverter(type, node.getAttributeRanges(changeKey, type));
		auto ranStr = msgBuild->CreateString(rangesStr);

		const std::string descriptionStr = node.getAttributeDescription(changeKey, type);
		auto descStr                     = msgBuild->CreateString(descriptionStr);

		dv::ConfigActionDataBuilder msg(*msgBuild);

		// Set message ID to the ID of the client that originated this change.
		// If we're running in any other thread it will be 0 (system), if the
		// change we're pushing comes from a listener firing in response to
		// changes brought by a client via the config-server, the current
		// client ID will be the one from that remote client. This is also
		// true for secondary effects like log messages.
		msg.add_id(server->getCurrentClientID());
		msg.add_action(dv::ConfigAction::PUSH_MESSAGE_ATTR);
		msg.add_attrEvents(static_cast<dv::ConfigAttributeEvents>(event));
		msg.add_node(nodeStr);
		msg.add_key(keyStr);
		msg.add_type(static_cast<dv::ConfigType>(type));
		msg.add_value(valStr);

		if ((event == DVCFG_ATTRIBUTE_ADDED) || (event == DVCFG_ATTRIBUTE_MODIFIED_CREATE)) {
			// Need to get extra info when adding: flags, range, description.
			msg.add_flags(flags);
			msg.add_ranges(ranStr);
			msg.add_description(descStr);
		}

		// Finish off message.
		auto msgRoot = msg.Finish();

		// Write root node and message size.
		dv::FinishSizePrefixedConfigActionDataBuffer(*msgBuild, msgRoot);

		server->pushMessageToClients(msgBuild);
	}
}
