#include "config_server_actions.h"

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

#include "../main.hpp"
#include "config_server_connection.h"

#include <boost/algorithm/string/join.hpp>
#include <boost/tokenizer.hpp>

#include <regex>
#include <thread>

static void dumpNodeToClientRecursive(
	const dv::Cfg::Node node, ConfigServerConnection *const client, const uint64_t receivedID);

template<typename MsgOps>
static inline void sendMessage(std::shared_ptr<ConfigServerConnection> client, MsgOps msgFunc) {
	// Send back flags directly.
	auto msgBuild = std::make_shared<flatbuffers::FlatBufferBuilder>(DV_CONFIG_SERVER_MAX_INCOMING_SIZE);

	// Build and then finish off message.
	auto msgRoot = msgFunc(msgBuild.get());

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

	client->writeMessage(msgBuild);
}

static inline void sendError(
	const std::string &errorMsg, std::shared_ptr<ConfigServerConnection> client, uint64_t receivedID) {
	sendMessage(client, [errorMsg, receivedID](flatbuffers::FlatBufferBuilder *msgBuild) {
		auto valStr = msgBuild->CreateString(errorMsg);

		dv::ConfigActionDataBuilder msg(*msgBuild);

		msg.add_action(dv::ConfigAction::CFG_ERROR);
		msg.add_id(receivedID);
		msg.add_value(valStr);

		return (msg.Finish());
	});

	dv::Log(dv::logLevel::DEBUG, "Client {:d}: sending ERROR response '{:s}'.", client->getClientID(), errorMsg);
}

static inline bool checkNodeExists(
	const std::string &node, std::shared_ptr<ConfigServerConnection> client, uint64_t receivedID) {
	bool nodeExists = dv::Cfg::GLOBAL.existsNode(node);

	// Only allow operations on existing nodes, this is for remote
	// control, so we only manipulate what's already there!
	if (!nodeExists) {
		// Send back error message to client.
		sendError("Node doesn't exist. Operations are only allowed on existing data.", client, receivedID);
	}

	return (nodeExists);
}

static inline bool checkAttributeExists(dv::Cfg::Node wantedNode, const std::string &key, dv::ConfigType type,
	std::shared_ptr<ConfigServerConnection> client, uint64_t receivedID) {
	// Check if attribute exists. Only allow operations on existing attributes!
	bool attrExists = wantedNode.existsAttribute(key, static_cast<dv::CfgType>(type));

	if (!attrExists) {
		// Send back error message to client.
		sendError(
			"Attribute of given type doesn't exist. Operations are only allowed on existing data.", client, receivedID);
	}

	return (attrExists);
}

static inline const std::string getString(const flatbuffers::String *str,
	std::shared_ptr<ConfigServerConnection> client, uint64_t receivedID, bool allowEmptyString = false) {
	// Check if member is not defined/missing.
	if (str == nullptr) {
		sendError("Required string member missing.", client, receivedID);
		throw std::invalid_argument("Required string member missing.");
	}

	std::string s(str->string_view());

	if (!allowEmptyString && s.empty()) {
		sendError("String member empty.", client, receivedID);
		throw std::invalid_argument("String member empty.");
	}

	return (s);
}

void ConfigServerHandleRequest(
	std::shared_ptr<ConfigServerConnection> client, std::unique_ptr<uint8_t[]> messageBuffer) {
	auto message = dv::GetConfigActionData(messageBuffer.get());

	dv::Log(dv::logLevel::DEBUG,
		"Client {:d}: handling request ID: {:d} - Action: {:s} - NodeEvents: {:s} - AttrEvents: {:s} - Node: {:s} - "
		"Key: {:s} "
		"- Type: {:s} - Value: {:s}.",
		client->getClientID(), message->id(), dv::EnumNameConfigAction(message->action()),
		dv::EnumNameConfigNodeEvents(message->nodeEvents()), dv::EnumNameConfigAttributeEvents(message->attrEvents()),
		(message->node() == nullptr) ? ("") : (message->node()->str()),
		(message->key() == nullptr) ? ("") : (message->key()->str()), dv::EnumNameConfigType(message->type()),
		(message->value() == nullptr) ? ("") : (message->value()->str()));

	dv::ConfigAction action = message->action();
	uint64_t receivedID     = message->id(); // Get incoming ID to send back.

	// Interpretation of data is up to each action individually.
	switch (action) {
		case dv::ConfigAction::NODE_EXISTS: {
			std::string node;

			try {
				node = getString(message->node(), client, receivedID);
			}
			catch (const std::invalid_argument &) {
				break;
			}

			// We only need the node name here. Type is not used (ignored)!
			bool result = dv::Cfg::GLOBAL.existsNode(node);

			// Send back result to client.
			sendMessage(client, [result, receivedID](flatbuffers::FlatBufferBuilder *msgBuild) {
				auto valStr = msgBuild->CreateString((result) ? ("true") : ("false"));

				dv::ConfigActionDataBuilder msg(*msgBuild);

				msg.add_action(dv::ConfigAction::NODE_EXISTS);
				msg.add_id(receivedID);
				msg.add_value(valStr);

				return (msg.Finish());
			});

			break;
		}

		case dv::ConfigAction::ATTR_EXISTS: {
			std::string node;
			std::string key;

			try {
				node = getString(message->node(), client, receivedID);
				key  = getString(message->key(), client, receivedID);
			}
			catch (const std::invalid_argument &) {
				break;
			}

			if (!checkNodeExists(node, client, receivedID)) {
				break;
			}

			// This cannot fail, since we know the node exists from above.
			dv::Cfg::Node wantedNode = dv::Cfg::GLOBAL.getNode(node);

			dv::ConfigType type = message->type();
			if (type == dv::ConfigType::UNKNOWN) {
				// If user doesn't specify a type, user doesn't care. We fetch type for her
				type = static_cast<dv::ConfigType>(wantedNode.getAttributeType(key));
			}
			// Check if attribute exists.
			bool result = wantedNode.existsAttribute(key, static_cast<dv::CfgType>(type));

			// Send back result to client.
			sendMessage(client, [result, receivedID](flatbuffers::FlatBufferBuilder *msgBuild) {
				auto valStr = msgBuild->CreateString((result) ? ("true") : ("false"));

				dv::ConfigActionDataBuilder msg(*msgBuild);

				msg.add_action(dv::ConfigAction::ATTR_EXISTS);
				msg.add_id(receivedID);
				msg.add_value(valStr);

				return (msg.Finish());
			});

			break;
		}

		case dv::ConfigAction::GET_CHILDREN: {
			std::string node;

			try {
				node = getString(message->node(), client, receivedID);
			}
			catch (const std::invalid_argument &) {
				break;
			}

			if (!checkNodeExists(node, client, receivedID)) {
				break;
			}

			// This cannot fail, since we know the node exists from above.
			dv::Cfg::Node wantedNode = dv::Cfg::GLOBAL.getNode(node);

			// We need to return a big string with all of the child names,
			// separated by a | character. No children will return empty string.
			const std::string namesString = boost::algorithm::join(wantedNode.getChildNames(), "|");

			sendMessage(client, [namesString, receivedID](flatbuffers::FlatBufferBuilder *msgBuild) {
				auto valStr = msgBuild->CreateString(namesString);

				dv::ConfigActionDataBuilder msg(*msgBuild);

				msg.add_action(dv::ConfigAction::GET_CHILDREN);
				msg.add_id(receivedID);
				msg.add_value(valStr);

				return (msg.Finish());
			});

			break;
		}

		case dv::ConfigAction::GET_ATTRIBUTES: {
			std::string node;

			try {
				node = getString(message->node(), client, receivedID);
			}
			catch (const std::invalid_argument &) {
				break;
			}

			if (!checkNodeExists(node, client, receivedID)) {
				break;
			}

			// This cannot fail, since we know the node exists from above.
			dv::Cfg::Node wantedNode = dv::Cfg::GLOBAL.getNode(node);

			// Get the keys of all the attributes and return them.
			auto attrKeys = wantedNode.getAttributeKeys();

			// No attributes at all, return empty.
			if (attrKeys.empty()) {
				// Send back error message to client.
				sendError("Node has no attributes.", client, receivedID);
				break;
			}

			// We need to return a big string with all of the attribute keys,
			// separated by a | character.
			const std::string attrKeysString = boost::algorithm::join(attrKeys, "|");

			sendMessage(client, [attrKeysString, receivedID](flatbuffers::FlatBufferBuilder *msgBuild) {
				auto valStr = msgBuild->CreateString(attrKeysString);

				dv::ConfigActionDataBuilder msg(*msgBuild);

				msg.add_action(dv::ConfigAction::GET_ATTRIBUTES);
				msg.add_id(receivedID);
				msg.add_value(valStr);

				return (msg.Finish());
			});

			break;
		}

		case dv::ConfigAction::GET_TYPE:        // intentional fallthrough
		case dv::ConfigAction::GET_RANGES:      // intentional fallthrough
		case dv::ConfigAction::GET_FLAGS:       // intentional fallthrough
		case dv::ConfigAction::GET_DESCRIPTION: // intentional fallthrough
		case dv::ConfigAction::GET: {
			std::string node;
			std::string key;

			try {
				node = getString(message->node(), client, receivedID);
				key  = getString(message->key(), client, receivedID);
			}
			catch (const std::invalid_argument &) {
				break;
			}

			if (!checkNodeExists(node, client, receivedID)) {
				break;
			}

			// This cannot fail, since we know the node exists from above.
			dv::Cfg::Node wantedNode = dv::Cfg::GLOBAL.getNode(node);

			auto type = message->type();
			if (type == dv::ConfigType::UNKNOWN) {
				// if user does not specify type, fetch it for the user
				type = static_cast<dv::ConfigType>(wantedNode.getAttributeType(key));
			}
			if (!checkAttributeExists(wantedNode, key, type, client, receivedID)) {
				break;
			}

			// get value
			union dvConfigAttributeValue result = wantedNode.getAttribute(key, static_cast<dv::CfgType>(type));
			const std::string resultStr
				= dv::Cfg::Helper::valueToStringConverter(static_cast<dv::CfgType>(type), result);

			if (type == dv::ConfigType::STRING) {
				free(result.string);
			}
			// get description
			std::string description  = wantedNode.getAttributeDescription(key, static_cast<dv::CfgType>(type));
			int flags                = wantedNode.getAttributeFlags(key, static_cast<dv::CfgType>(type));
			const std::string ranges = dv::Cfg::Helper::rangesToStringConverter(
				static_cast<dv::CfgType>(type), wantedNode.getAttributeRanges(key, static_cast<dv::CfgType>(type)));

			sendMessage(client, [action, resultStr, ranges, flags, description, receivedID, type](
									flatbuffers::FlatBufferBuilder *msgBuild) {
				auto valStr         = msgBuild->CreateString(resultStr);
				auto rangeStr       = msgBuild->CreateString(ranges);
				auto descriptionStr = msgBuild->CreateString(description);

				dv::ConfigActionDataBuilder msg(*msgBuild);

				msg.add_action(action);
				msg.add_id(receivedID);
				msg.add_value(valStr);
				msg.add_type(type);
				msg.add_description(descriptionStr);
				msg.add_ranges(rangeStr);
				msg.add_flags(flags);

				return (msg.Finish());
			});

			break;
		}

		case dv::ConfigAction::PUT: {
			std::string node;
			std::string key;
			std::string value;

			try {
				node = getString(message->node(), client, receivedID);
				key  = getString(message->key(), client, receivedID);
			}
			catch (const std::invalid_argument &) {
				break;
			}

			// Support creating new nodes.
			const bool import = (static_cast<uint32_t>(message->flags()) & DVCFG_FLAGS_IMPORTED);

			if (!import && !checkNodeExists(node, client, receivedID)) {
				break;
			}

			// This cannot fail, since we know the node exists from above.
			dv::Cfg::Node wantedNode = dv::Cfg::GLOBAL.getNode(node);
			auto type                = message->type();
			if (type == dv::ConfigType::UNKNOWN) {
				// if user does not specify type, fetch it for the user
				type = static_cast<dv::ConfigType>(wantedNode.getAttributeType(key));
			}

			try {
				value = getString(message->value(), client, receivedID, true);
			}
			catch (const std::invalid_argument &) {
				break;
			}

			if (!import && !checkAttributeExists(wantedNode, key, type, client, receivedID)) {
				break;
			}

			// Put given value into config node. Node, attr and type are already verified.
			const std::string typeStr = dv::Cfg::Helper::typeToStringConverter(static_cast<dv::CfgType>(type));

			if (!wantedNode.stringToAttributeConverter(key, typeStr, value)) {
				// Send back correct error message to client.
				if (errno == EINVAL) {
					sendError("Impossible to convert value according to type.", client, receivedID);
					break;
				}
				else if (errno == EPERM) {
					// Suppress error message on initial import.
					// It is supposed to not overwrite READ_ONLY attributes ever.
					if (!import) {
						sendError("Cannot write to a read-only attribute.", client, receivedID);
						break;
					}
				}
				else if (errno == ERANGE) {
					sendError("Value out of attribute range.", client, receivedID);
					break;
				}
				else {
					// Unknown error.
					sendError("Unknown error.", client, receivedID);
					break;
				}
			}

			// Send back confirmation to the client.
			sendMessage(client, [receivedID](flatbuffers::FlatBufferBuilder *msgBuild) {
				dv::ConfigActionDataBuilder msg(*msgBuild);

				msg.add_action(dv::ConfigAction::PUT);
				msg.add_id(receivedID);

				return (msg.Finish());
			});

			break;
		}

		case dv::ConfigAction::ADD_MODULE: {
			std::string moduleName;
			std::string moduleLibrary;

			try {
				moduleName    = getString(message->node(), client, receivedID);
				moduleLibrary = getString(message->key(), client, receivedID);
			}
			catch (const std::invalid_argument &) {
				break;
			}

			const std::regex moduleNameRegex("^[a-zA-Z-_\\d\\.]+$");

			if (!std::regex_match(moduleName, moduleNameRegex)) {
				sendError("Name uses invalid characters.", client, receivedID);
				break;
			}

			if (dv::Cfg::GLOBAL.existsNode("/mainloop/" + moduleName + "/")) {
				sendError("Name is already in use.", client, receivedID);
				break;
			}

			// Check module library.
			auto modulesSysNode = dv::Cfg::GLOBAL.getNode("/system/modules/");
			auto modulesList    = modulesSysNode.getChildNames();

			if (!dv::vectorContains(modulesList, moduleLibrary)) {
				sendError("Library does not exist.", client, receivedID);
				break;
			}

			// Name and library are fine, create the module.
			dv::addModule(moduleName, moduleLibrary, true);

			// Send back confirmation to the client.
			sendMessage(client, [receivedID](flatbuffers::FlatBufferBuilder *msgBuild) {
				dv::ConfigActionDataBuilder msg(*msgBuild);

				msg.add_action(dv::ConfigAction::ADD_MODULE);
				msg.add_id(receivedID);

				return (msg.Finish());
			});

			break;
		}

		case dv::ConfigAction::REMOVE_MODULE: {
			std::string moduleName;

			try {
				moduleName = getString(message->node(), client, receivedID);
			}
			catch (const std::invalid_argument &) {
				break;
			}

			if (!dv::Cfg::GLOBAL.existsNode("/mainloop/" + moduleName + "/")) {
				sendError("Name is not in use.", client, receivedID);
				break;
			}

			// Truly delete the node and all its children.
			dv::removeModule(moduleName);

			// Send back confirmation to the client.
			sendMessage(client, [receivedID](flatbuffers::FlatBufferBuilder *msgBuild) {
				dv::ConfigActionDataBuilder msg(*msgBuild);

				msg.add_action(dv::ConfigAction::REMOVE_MODULE);
				msg.add_id(receivedID);

				return (msg.Finish());
			});

			break;
		}

		case dv::ConfigAction::ADD_PUSH_CLIENT: {
			// Send back confirmation to the client.
			uint64_t clientID = client->getClientID();
			sendMessage(client, [receivedID, clientID](flatbuffers::FlatBufferBuilder *msgBuild) {
				auto clientIdStr = msgBuild->CreateString(std::to_string(clientID));

				dv::ConfigActionDataBuilder msg(*msgBuild);

				msg.add_action(dv::ConfigAction::ADD_PUSH_CLIENT);
				msg.add_id(receivedID);
				msg.add_value(clientIdStr);

				return (msg.Finish());
			});

			// Only add client after sending confirmation, so no PUSH
			// messages may arrive before the client sees the confirmation.
			client->addPushClient();

			break;
		}

		case dv::ConfigAction::REMOVE_PUSH_CLIENT: {
			// Remove client first, so that after confirmation of removal
			// no more PUSH messages may arrive.
			client->removePushClient();

			// Send back confirmation to the client.
			sendMessage(client, [receivedID](flatbuffers::FlatBufferBuilder *msgBuild) {
				dv::ConfigActionDataBuilder msg(*msgBuild);

				msg.add_action(dv::ConfigAction::REMOVE_PUSH_CLIENT);
				msg.add_id(receivedID);

				return (msg.Finish());
			});

			break;
		}

		case dv::ConfigAction::DUMP_TREE: {
			// Run through the whole ConfigTree as it is currently and dump its content.
			dumpNodeToClientRecursive(dv::Cfg::GLOBAL.getRootNode(), client.get(), receivedID);

			// Send back confirmation of operation completed to the client.
			sendMessage(client, [receivedID](flatbuffers::FlatBufferBuilder *msgBuild) {
				dv::ConfigActionDataBuilder msg(*msgBuild);

				msg.add_action(dv::ConfigAction::DUMP_TREE);
				msg.add_id(receivedID);

				return (msg.Finish());
			});

			break;
		}

		case dv::ConfigAction::GET_CLIENT_ID: {
			uint64_t clientID = client->getClientID();

			// Send back confirmation of operation completed to the client.
			sendMessage(client, [clientID](flatbuffers::FlatBufferBuilder *msgBuild) {
				dv::ConfigActionDataBuilder msg(*msgBuild);

				msg.add_action(dv::ConfigAction::GET_CLIENT_ID);
				msg.add_id(clientID);

				return (msg.Finish());
			});

			break;
		}

		default: {
			// Unknown action, send error back to client.
			sendError("Unknown action.", client, receivedID);

			break;
		}
	}
}

static void dumpNodeToClientRecursive(
	const dv::Cfg::Node node, ConfigServerConnection *const client, const uint64_t receivedID) {
	// Dump node path.
	{
		auto msgBuild = std::make_shared<flatbuffers::FlatBufferBuilder>(DV_CONFIG_SERVER_MAX_INCOMING_SIZE);

		auto nodeStr = msgBuild->CreateString(node.getPath());

		dv::ConfigActionDataBuilder msg(*msgBuild);

		msg.add_action(dv::ConfigAction::DUMP_TREE_NODE);
		msg.add_id(receivedID);
		msg.add_node(nodeStr);

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

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

		client->writePushMessage(msgBuild);
	}

	// Dump all attribute keys.
	for (const auto &key : node.getAttributeKeys()) {
		auto msgBuild = std::make_shared<flatbuffers::FlatBufferBuilder>(DV_CONFIG_SERVER_MAX_INCOMING_SIZE);

		auto type  = node.getAttributeType(key);
		auto flags = node.getAttributeFlags(key, type);

		union dvConfigAttributeValue value = node.getAttribute(key, type);
		const std::string valueStr         = dv::Cfg::Helper::valueToStringConverter(type, value);
		if (type == dv::CfgType::STRING) {
			free(value.string);
		}

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

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

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

		dv::ConfigActionDataBuilder msg(*msgBuild);

		msg.add_action(dv::ConfigAction::DUMP_TREE_ATTR);
		msg.add_id(receivedID);
		msg.add_node(nodeStr);
		msg.add_key(keyStr);
		msg.add_type(static_cast<dv::ConfigType>(type));
		msg.add_value(valStr);

		// 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);

		client->writePushMessage(msgBuild);
	}

	// Recurse over all children.
	for (const auto &child : node.getChildren()) {
		dumpNodeToClientRecursive(child, client, receivedID);
	}
}
