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

#include "../../src/config_server/dv_config_action_data.hpp"
#include "../ext/linenoise-ng/linenoise.h"

// clang-format off
// Bug in boost::asio < 1.79.0, missing include.
#include <utility>
// clang-format on

#include <boost/algorithm/string/join.hpp>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/args.hpp>
#include <boost/nowide/filesystem.hpp>
#include <boost/nowide/iostream.hpp>
#include <boost/program_options.hpp>
#include <boost/tokenizer.hpp>

#include <iostream>
#include <string>
#include <vector>

namespace asio    = boost::asio;
namespace asioSSL = boost::asio::ssl;
namespace asioIP  = boost::asio::ip;
using asioTCP     = boost::asio::ip::tcp;
namespace po      = boost::program_options;

#define DVCTL_HISTORY_FILE_NAME      ".dv-control.history"
#define DVCTL_CLIENT_BUFFER_MAX_SIZE 8192

#define MAX_CMD_PARTS 4

#define CMD_PART_ACTION 0
#define CMD_PART_NODE   1
#define CMD_PART_KEY    2
#define CMD_PART_VALUE  3

static void handleInputLine(const std::string &inputLine);
static void handleCommandCompletion(const char *buf, linenoiseCompletions *autoComplete);

static void actionCompletion(
	const std::string &buf, linenoiseCompletions *autoComplete, const std::string &partialActionString);
static void nodeCompletion(const std::string &buf, linenoiseCompletions *autoComplete, dv::ConfigAction action,
	const std::string &partialNodeString);
static void keyCompletion(const std::string &buf, linenoiseCompletions *autoComplete, dv::ConfigAction action,
	const std::string &nodeString, const std::string &partialKeyString);
static void valueCompletion(const std::string &buf, linenoiseCompletions *autoComplete, dv::ConfigAction action,
	const std::string &nodeString, const std::string &keyString, const std::string &partialValueString);
static void addCompletionSuffix(linenoiseCompletions *autocomplete, const char *buf, size_t completionPoint,
	const char *suffix, bool endSpace, bool endSlash);

static const struct {
	const std::string name;
	const dv::ConfigAction code;
} actions[] = {
	{"node_exists",     dv::ConfigAction::NODE_EXISTS    },
	{"attr_exists",     dv::ConfigAction::ATTR_EXISTS    },
	{"get",			 dv::ConfigAction::GET            },
	{"put",			 dv::ConfigAction::PUT            },
	{"set",			 dv::ConfigAction::PUT            },
	{"help",            dv::ConfigAction::GET_DESCRIPTION},
	{"get_description", dv::ConfigAction::GET_DESCRIPTION},
	{"get_type",        dv::ConfigAction::GET_TYPE       },
	{"add_module",      dv::ConfigAction::ADD_MODULE     },
	{"remove_module",   dv::ConfigAction::REMOVE_MODULE  },
	{"get_client_id",   dv::ConfigAction::GET_CLIENT_ID  },
	{"dump_tree",       dv::ConfigAction::DUMP_TREE      },
	{"get_children",    dv::ConfigAction::GET_CHILDREN   },
};

static flatbuffers::FlatBufferBuilder dataBufferSend{DVCTL_CLIENT_BUFFER_MAX_SIZE};
static uint8_t dataBufferReceive[DVCTL_CLIENT_BUFFER_MAX_SIZE];

static asio::io_context ioService;
static asioSSL::context tlsContext{asioSSL::context::tlsv12_client};
static asioSSL::stream<asioTCP::socket> tlsSocket{ioService, tlsContext};
static bool secureConnection{false};

static bool runLoop{true};

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

// Support different boost versions.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant"

static inline void asioSocketWrite(const asio::const_buffer &buf) {
	const asio::const_buffer buf2(buf);

	if (secureConnection) {
		asio::write(tlsSocket, buf2);
	}
	else {
		asio::write(tlsSocket.next_layer(), buf2);
	}
}

static inline void asioSocketRead(const asio::mutable_buffer &buf) {
	const asio::mutable_buffer buf2(buf);

	if (secureConnection) {
		asio::read(tlsSocket, buf2);
	}
	else {
		asio::read(tlsSocket.next_layer(), buf2);
	}
}

#pragma GCC diagnostic pop

static inline void sendMessage(std::unique_ptr<dv::ConfigActionDataBuilder> msg) {
	// Finish off message.
	auto msgRoot = msg->Finish();

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

	// Send data out over the network.
	asioSocketWrite(asio::buffer(dataBufferSend.GetBufferPointer(), dataBufferSend.GetSize()));

	// Clear flatbuffer for next usage.
	dataBufferSend.Clear();
}

static inline const dv::ConfigActionDataFlatbuffer *receiveMessage() {
	flatbuffers::uoffset_t incomingMessageSize;

	// Get message size.
	asioSocketRead(asio::buffer(&incomingMessageSize, sizeof(incomingMessageSize)));

	// Check for wrong (excessive) message length.
	if (incomingMessageSize > DVCTL_CLIENT_BUFFER_MAX_SIZE) {
		throw std::runtime_error("Message length error (%d bytes).");
	}

	// Get actual data.
	asioSocketRead(asio::buffer(dataBufferReceive, incomingMessageSize));

	// Now we have the flatbuffer message and can verify it.
	flatbuffers::Verifier verifyMessage(dataBufferReceive, incomingMessageSize);

	if (!dv::VerifyConfigActionDataBuffer(verifyMessage)) {
		// Failed verification.
		throw std::runtime_error("Message verification error.");
	}

	auto message = dv::GetConfigActionData(dataBufferReceive);

	return (message);
}

int main(int argc, char *argv[]) {
	// UTF-8 support for file paths and CLI arguments on Windows.
	boost::nowide::nowide_filesystem();
	boost::nowide::args utfArgs(argc, argv);

	// Allowed command-line options for dv-control.
	po::options_description cliDescription("Command-line options");
	cliDescription.add_options()("help,h", "print help text")("ipaddress,i", po::value<std::string>(),
		"IP-address or hostname to connect to")("port,p", po::value<std::string>(), "port to connect to")("tls",
		po::value<std::string>()->implicit_value(""),
		"enable TLS for connection (no argument uses default CA for verification, or pass a path to a specific CA file "
		"in the PEM format)")(
		"tlscert", po::value<std::string>(), "TLS certificate file for client authentication (PEM format)")(
		"tlskey", po::value<std::string>(), "TLS key file for client authentication (PEM format)")("script,s",
		po::value<std::vector<std::string>>()->multitoken(),
		"script mode, sends the given command directly to the server as if typed in and exits.\n"
		"Format: <action> <node> [<attribute> [<value>]]\nExample: set /system/logger/ logLevel DEBUG");

	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")) {
		printHelpAndExit(cliDescription);
	}

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

	std::string portNumber("4040");
	if (cliVarMap.count("port")) {
		portNumber = cliVarMap["port"].as<std::string>();
	}

	if (cliVarMap.count("tls")) {
		secureConnection = true;

		// Client-side TLS authentication support.
		if (cliVarMap.count("tlscert")) {
			std::string tlsCertFile = cliVarMap["tlscert"].as<std::string>();

			try {
				tlsContext.use_certificate_chain_file(tlsCertFile);
			}
			catch (const boost::system::system_error &ex) {
				boost::nowide::cerr << fmt::format(
					FMT_STRING("Failed to load TLS client certificate file '{:s}', error message is:\n\t{:s}."),
					tlsCertFile, ex.what())
									<< std::endl;
				return (EXIT_FAILURE);
			}
		}

		if (cliVarMap.count("tlskey")) {
			std::string tlsKeyFile = cliVarMap["tlskey"].as<std::string>();

			try {
				tlsContext.use_private_key_file(tlsKeyFile, asioSSL::context::pem);
			}
			catch (const boost::system::system_error &ex) {
				boost::nowide::cerr << fmt::format(
					FMT_STRING("Failed to load TLS client key file '{:s}', error message is:\n\t{:s}."), tlsKeyFile,
					ex.what()) << std::endl;
				return (EXIT_FAILURE);
			}
		}

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

		std::string tlsVerifyFile = cliVarMap["tls"].as<std::string>();

		if (tlsVerifyFile.empty()) {
			tlsContext.set_default_verify_paths();
		}
		else {
			try {
				tlsContext.load_verify_file(tlsVerifyFile);
			}
			catch (const boost::system::system_error &ex) {
				boost::nowide::cerr << fmt::format(
					FMT_STRING("Failed to load TLS CA verification file '{:s}', error message is:\n\t{:s}."),
					tlsVerifyFile, ex.what())
									<< std::endl;
				return (EXIT_FAILURE);
			}
		}

		tlsContext.set_verify_mode(asioSSL::context::verify_peer);

		// Rebuild TLS socket, so it picks up changes to TLS context above.
		new (&tlsSocket) asioSSL::stream<asioTCP::socket>(ioService, tlsContext);
	}

	bool scriptMode = false;
	if (cliVarMap.count("script")) {
		std::vector<std::string> commandComponents = cliVarMap["script"].as<std::vector<std::string>>();

		// At least one command component must be passed, any less is an error.
		if (commandComponents.empty()) {
			boost::nowide::cout << "Script mode must have at least one command component!" << std::endl;
			printHelpAndExit(cliDescription);
		}

		// At most four components can be passed, any more is an error.
		if (commandComponents.size() > MAX_CMD_PARTS) {
			boost::nowide::cout << "Script mode cannot have more than four components!" << std::endl;
			printHelpAndExit(cliDescription);
		}

		if ((commandComponents[0] == "quit") || (commandComponents[0] == "exit")) {
			boost::nowide::cout << "Script mode cannot use 'quit' or 'exit' actions!" << std::endl;
			printHelpAndExit(cliDescription);
		}

		scriptMode = true;
	}

	// Generate command history file path (in user home).
	boost::filesystem::path commandHistoryFilePath;

	try {
		commandHistoryFilePath = dv::portable_get_user_home_directory();
	}
	catch (const std::runtime_error &) {
		boost::nowide::cerr << "Failed to get home directory for history file, using current working directory."
							<< std::endl;
		commandHistoryFilePath = boost::filesystem::current_path();
	}

	commandHistoryFilePath /= DVCTL_HISTORY_FILE_NAME;

	// Connect to the remote DV config server.
	try {
		asioTCP::resolver resolver(ioService);

// Support different boost versions.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant"
		asio::connect(tlsSocket.next_layer(), resolver.resolve(ipAddress, portNumber));
#pragma GCC diagnostic pop
	}
	catch (const boost::system::system_error &ex) {
		boost::nowide::cerr << fmt::format(
			FMT_STRING("Failed to connect to {:s}:{:s}, error message is:\n\t{:s}."), ipAddress, portNumber, ex.what())
							<< std::endl;
		return (EXIT_FAILURE);
	}

	// Secure connection support.
	if (secureConnection) {
		try {
			tlsSocket.handshake(asioSSL::stream_base::client);
		}
		catch (const boost::system::system_error &ex) {
			boost::nowide::cerr << fmt::format(FMT_STRING("Failed TLS handshake, error message is:\n\t{:s}."),
				ex.what()) << std::endl;
			return (EXIT_FAILURE);
		}
	}

	// Load command history file.
	linenoiseHistoryLoad(commandHistoryFilePath.string().c_str());

	if (scriptMode) {
		std::vector<std::string> commandComponents = cliVarMap["script"].as<std::vector<std::string>>();

		// If value component contains spaces or is empty, surround with
		// double quotes to trigger correct handling of spaces in values.
		if (commandComponents.size() == MAX_CMD_PARTS) {
			const auto valueString = commandComponents[CMD_PART_VALUE];

			if (valueString.empty() || (valueString.find(' ') != std::string::npos)) {
				commandComponents[CMD_PART_VALUE] = '"' + valueString + '"';
			}
		}

		const std::string inputString = boost::algorithm::join(commandComponents, " ");

		if (!inputString.empty()) {
			// Add input to command history.
			linenoiseHistoryAdd(inputString.c_str());

			// Try to generate a request, if there's any content.
			handleInputLine(inputString);
		}
	}
	else {
		// Create a shell prompt with the IP:Port displayed.
		const auto shellPrompt = fmt::format(FMT_STRING("DV @ {:s}:{:s} >> "), ipAddress, portNumber);

		// Set our own command completion function.
		linenoiseSetCompletionCallback(&handleCommandCompletion);

		while (runLoop) {
			// Display prompt and read input (NOTE: remember to free input after use!).
			char *inputLine = linenoise(shellPrompt.c_str());

			// Check for EOF first.
			if (inputLine == nullptr) {
				// Exit loop.
				runLoop = false;
				continue;
			}

			const std::string inputString = inputLine;

			// Free input after use.
			free(inputLine);

			if (!inputString.empty()) {
				// Add input to command history.
				linenoiseHistoryAdd(inputString.c_str());

				// Then, after having added to history, check for termination commands.
				if ((inputString == "quit") || (inputString == "exit")) {
					// Exit loop.
					runLoop = false;
					continue;
				}

				// Try to generate a request, if there's any content.
				handleInputLine(inputString);
			}
		}
	}

	// Save command history file.
	linenoiseHistorySave(commandHistoryFilePath.string().c_str());

	// Close secure connection properly.
	if (secureConnection) {
		boost::system::error_code error;
		tlsSocket.shutdown(error);

		// EOF is expected for good TLS shutdown. See:
		// https://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error
		if (error != asio::error::eof) {
			boost::nowide::cerr << fmt::format(FMT_STRING("Failed TLS shutdown, error message is:\n\t{:s}."),
				error.message()) << std::endl;
		}
	}

	return (EXIT_SUCCESS);
}

static void handleInputLine(const std::string &inputLine) {
	const std::string escSep{""};   // no escape character
	const std::string delim{" \t"}; // split on spaces and tabs
	const std::string quotes{"\""}; // allow double-quoted values within delimiters

	boost::escaped_list_separator<char> separator(escSep, delim, quotes);
	boost::tokenizer<boost::escaped_list_separator<char>> lineTokens(inputLine, separator);

	// Gather non-empty tokens for later parsing.
	std::vector<std::string> commandParts;

	for (const auto &tok : lineTokens) {
		if (tok.empty()) {
			continue;
		}

		commandParts.push_back(tok);
	}

	// Check that we got something.
	if (commandParts.empty()) {
		boost::nowide::cerr << "Error: empty command." << std::endl;
		return;
	}

	if (commandParts.size() > MAX_CMD_PARTS) {
		// Abort, too many parts.
		boost::nowide::cerr << "Error: command is made up of too many parts." << std::endl;
		return;
	}

	// Let's get the action code first thing.
	dv::ConfigAction action = dv::ConfigAction::CFG_ERROR;

	for (const auto &act : actions) {
		if (act.name == commandParts[CMD_PART_ACTION]) {
			action = act.code;
		}
	}

	// Initialize buffer.
	std::unique_ptr<dv::ConfigActionDataBuilder> msg{nullptr};

	// Now that we know what we want to do, let's decode the command line.
	switch (action) {
		case dv::ConfigAction::GET_CLIENT_ID:
		case dv::ConfigAction::DUMP_TREE: {
			// No parameters needed.
			if (commandParts.size() > (CMD_PART_ACTION + 1)) {
				boost::nowide::cerr << "Error: too many parameters for command." << std::endl;
				return;
			}

			msg = std::make_unique<dv::ConfigActionDataBuilder>(dataBufferSend);
			msg->add_action(action);

			break;
		}

		case dv::ConfigAction::NODE_EXISTS:
		case dv::ConfigAction::GET_CHILDREN: {
			// Check parameters needed for operation.
			if (commandParts.size() == CMD_PART_NODE) {
				boost::nowide::cerr << "Error: missing node parameter." << std::endl;
				return;
			}
			if (commandParts.size() > (CMD_PART_NODE + 1)) {
				boost::nowide::cerr << "Error: too many parameters for command." << std::endl;
				return;
			}

			auto nodeOffset = dataBufferSend.CreateString(commandParts[CMD_PART_NODE]);

			msg = std::make_unique<dv::ConfigActionDataBuilder>(dataBufferSend);
			msg->add_action(action);
			msg->add_node(nodeOffset);

			break;
		}

		case dv::ConfigAction::ATTR_EXISTS:
		case dv::ConfigAction::GET:
		case dv::ConfigAction::GET_TYPE:
		case dv::ConfigAction::GET_DESCRIPTION: {
			// Check parameters needed for operation.
			if (commandParts.size() == CMD_PART_NODE) {
				boost::nowide::cerr << "Error: missing node parameter." << std::endl;
				return;
			}
			if (commandParts.size() == CMD_PART_KEY) {
				boost::nowide::cerr << "Error: missing key parameter." << std::endl;
				return;
			}
			if (commandParts.size() > (CMD_PART_KEY + 1)) {
				boost::nowide::cerr << "Error: too many parameters for command." << std::endl;
				return;
			}

			auto nodeOffset = dataBufferSend.CreateString(commandParts[CMD_PART_NODE]);
			auto keyOffset  = dataBufferSend.CreateString(commandParts[CMD_PART_KEY]);

			msg = std::make_unique<dv::ConfigActionDataBuilder>(dataBufferSend);
			msg->add_action(action);
			msg->add_node(nodeOffset);
			msg->add_key(keyOffset);
			msg->add_type(static_cast<dv::ConfigType>(dv::Config::AttributeType::UNKNOWN));

			break;
		}

		case dv::ConfigAction::PUT: {
			// Check parameters needed for operation.
			if (commandParts.size() == CMD_PART_NODE) {
				boost::nowide::cerr << "Error: missing node parameter." << std::endl;
				return;
			}
			if (commandParts.size() == CMD_PART_KEY) {
				boost::nowide::cerr << "Error: missing key parameter." << std::endl;
				return;
			}
			if (commandParts.size() == CMD_PART_VALUE) {
				// Missing value interpreted as empty string.
				commandParts.push_back("");
			}
			if (commandParts.size() > (CMD_PART_VALUE + 1)) {
				boost::nowide::cerr << "Error: too many parameters for command." << std::endl;
				return;
			}

			if ((commandParts[CMD_PART_NODE] == "/system/") && (commandParts[CMD_PART_KEY] == "running")
				&& (commandParts[CMD_PART_VALUE] == "false")) {
				runLoop = false;
			}

			auto nodeOffset  = dataBufferSend.CreateString(commandParts[CMD_PART_NODE]);
			auto keyOffset   = dataBufferSend.CreateString(commandParts[CMD_PART_KEY]);
			auto valueOffset = dataBufferSend.CreateString(commandParts[CMD_PART_VALUE]);

			msg = std::make_unique<dv::ConfigActionDataBuilder>(dataBufferSend);
			msg->add_action(action);
			msg->add_node(nodeOffset);
			msg->add_key(keyOffset);
			msg->add_type(static_cast<dv::ConfigType>(dv::Config::AttributeType::UNKNOWN));
			msg->add_value(valueOffset);

			break;
		}

		case dv::ConfigAction::ADD_MODULE: {
			// Check parameters needed for operation. Reuse node parameters.
			if (commandParts.size() == CMD_PART_NODE) {
				boost::nowide::cerr << "Error: missing module name." << std::endl;
				return;
			}
			if (commandParts.size() == CMD_PART_KEY) {
				boost::nowide::cerr << "Error: missing library name." << std::endl;
				return;
			}
			if (commandParts.size() > (CMD_PART_KEY + 1)) {
				boost::nowide::cerr << "Error: too many parameters for command." << std::endl;
				return;
			}

			auto nodeOffset = dataBufferSend.CreateString(commandParts[CMD_PART_NODE]);
			auto keyOffset  = dataBufferSend.CreateString(commandParts[CMD_PART_KEY]);

			msg = std::make_unique<dv::ConfigActionDataBuilder>(dataBufferSend);
			msg->add_action(action);
			msg->add_node(nodeOffset);
			msg->add_key(keyOffset);

			break;
		}

		case dv::ConfigAction::REMOVE_MODULE: {
			// Check parameters needed for operation. Reuse node parameters.
			if (commandParts.size() == CMD_PART_NODE) {
				boost::nowide::cerr << "Error: missing module name." << std::endl;
				return;
			}
			if (commandParts.size() > (CMD_PART_NODE + 1)) {
				boost::nowide::cerr << "Error: too many parameters for command." << std::endl;
				return;
			}

			auto nodeOffset = dataBufferSend.CreateString(commandParts[CMD_PART_NODE]);

			msg = std::make_unique<dv::ConfigActionDataBuilder>(dataBufferSend);
			msg->add_action(action);
			msg->add_node(nodeOffset);

			break;
		}

		default:
			boost::nowide::cerr << "Error: unknown command." << std::endl;
			return;
	}

	// Send formatted command to configuration server.
	try {
		sendMessage(std::move(msg));
	}
	catch (const boost::system::system_error &ex) {
		boost::nowide::cerr << fmt::format(
			FMT_STRING("Unable to send data to config server, error message is:\n\t{:s}."), ex.what())
							<< std::endl;
		return;
	}

	const dv::ConfigActionDataFlatbuffer *resp = nullptr;

	try {
		resp = receiveMessage();
	}
	catch (const boost::system::system_error &ex) {
		boost::nowide::cerr << fmt::format(
			FMT_STRING("Unable to receive data from config server, error message is:\n\t{:s}."), ex.what())
							<< std::endl;
		return;
	}

	if (action == dv::ConfigAction::DUMP_TREE) {
		while (true) {
			// Continue getting messages till finished.
			// End marked by confirmation message with same action.
			if (resp->action() == dv::ConfigAction::DUMP_TREE_NODE) {
				boost::nowide::cout << "NODE: " << resp->node()->string_view() << std::endl;
			}
			else if (resp->action() == dv::ConfigAction::DUMP_TREE_ATTR) {
				boost::nowide::cout << "ATTR: " << resp->node()->string_view() << " | " << resp->key()->string_view()
									<< ", "
									<< dv::Config::Helper::typeToStringConverter(
										   static_cast<dv::Config::AttributeType>(resp->type()))
									<< " | " << resp->value()->string_view() << std::endl;
			}
			else if (resp->action() == dv::ConfigAction::DUMP_TREE) {
				// Done, confirmation received.
				break;
			}
			else {
				// Unexpected action.
				boost::nowide::cerr << fmt::format(
					FMT_STRING("Unknown action '{:s}' during DUMP_TREE."), dv::EnumNameConfigAction(resp->action()))
									<< std::endl;
				return;
			}

			try {
				resp = receiveMessage();
			}
			catch (const boost::system::system_error &ex) {
				boost::nowide::cerr << fmt::format(
					FMT_STRING("Unable to receive data from config server, error message is:\n\t{:s}."), ex.what())
									<< std::endl;
				return;
			}
		}
	}

	// Display results.
	switch (resp->action()) {
		case dv::ConfigAction::CFG_ERROR:
			// Return error in 'value'.
			boost::nowide::cerr << "ERROR on " << dv::EnumNameConfigAction(action) << ": "
								<< resp->value()->string_view() << std::endl;
			break;

		case dv::ConfigAction::NODE_EXISTS:
		case dv::ConfigAction::ATTR_EXISTS:
		case dv::ConfigAction::GET:
		case dv::ConfigAction::GET_CHILDREN:
			// 'value' contains results in string format, use directly.
			boost::nowide::cout << resp->value()->string_view() << std::endl;
			break;

		case dv::ConfigAction::GET_DESCRIPTION:
			// Return help text in 'description'.
			boost::nowide::cout << resp->description()->string_view() << std::endl;
			break;

		case dv::ConfigAction::GET_TYPE:
			// Return help text in 'description'.
			boost::nowide::cout << dv::Config::Helper::typeToStringConverter(static_cast<dv::Config::AttributeType>(
				resp->type())) << std::endl;
			break;

		case dv::ConfigAction::GET_CLIENT_ID:
			// Return 64bit client ID in 'id'.
			boost::nowide::cout << resp->id() << std::endl;
			break;

		default:
			// No return value, don't print anything.
			break;
	}
}

static void handleCommandCompletion(const char *buf, linenoiseCompletions *autoComplete) {
	const std::string commandStr{buf};

	const std::string escSep{""};   // no escape character
	const std::string delim{" \t"}; // split on spaces and tabs
	const std::string quotes{"\""}; // allow double-quoted values within delimiters

	boost::escaped_list_separator<char> separator(escSep, delim, quotes);
	boost::tokenizer<boost::escaped_list_separator<char>> lineTokens(commandStr, separator);

	// Gather non-empty tokens for later parsing.
	std::vector<std::string> commandParts;

	for (const auto &tok : lineTokens) {
		if (tok.empty()) {
			continue;
		}

		commandParts.push_back(tok);
	}

	// Also calculate number of commands already present in line (word-depth).
	// This is actually much more useful to understand where we are and what to do.
	size_t commandDepth = commandParts.size();

	if ((commandDepth > 0) && (!commandStr.empty()) && (commandStr.back() != ' ')) {
		// If commands are present, ensure they have been "confirmed" by at least
		// one terminating spacing character. Else don't calculate the last command.
		commandDepth--;
	}

	// Ensure empty string is present if that part wasn't completed yet.
	if (commandParts.size() <= commandDepth) {
		commandParts.push_back("");
	}

	// Check that we got something.
	if (commandDepth == 0) {
		// Always start off with a command/action.
		actionCompletion(commandStr, autoComplete, commandParts[CMD_PART_ACTION]);

		return;
	}

	// Let's get the action code first thing.
	dv::ConfigAction action = dv::ConfigAction::CFG_ERROR;

	for (const auto &act : actions) {
		if (act.name == commandParts[CMD_PART_ACTION]) {
			action = act.code;
		}
	}

	switch (action) {
		case dv::ConfigAction::NODE_EXISTS:
		case dv::ConfigAction::GET_CHILDREN:
			if (commandDepth == 1) {
				nodeCompletion(commandStr, autoComplete, action, commandParts[CMD_PART_NODE]);
			}

			break;

		case dv::ConfigAction::ATTR_EXISTS:
		case dv::ConfigAction::GET:
		case dv::ConfigAction::GET_TYPE:
		case dv::ConfigAction::GET_DESCRIPTION:
			if (commandDepth == 1) {
				nodeCompletion(commandStr, autoComplete, action, commandParts[CMD_PART_NODE]);
			}
			if (commandDepth == 2) {
				keyCompletion(
					commandStr, autoComplete, action, commandParts[CMD_PART_NODE], commandParts[CMD_PART_KEY]);
			}

			break;

		case dv::ConfigAction::PUT:
			if (commandDepth == 1) {
				nodeCompletion(commandStr, autoComplete, action, commandParts[CMD_PART_NODE]);
			}
			if (commandDepth == 2) {
				keyCompletion(
					commandStr, autoComplete, action, commandParts[CMD_PART_NODE], commandParts[CMD_PART_KEY]);
			}
			if (commandDepth == 3) {
				valueCompletion(commandStr, autoComplete, action, commandParts[CMD_PART_NODE],
					commandParts[CMD_PART_KEY], commandParts[CMD_PART_VALUE]);
			}

			break;

		default:
			break;
	}
}

static void actionCompletion(
	const std::string &buf, linenoiseCompletions *autoComplete, const std::string &partialActionString) {
	UNUSED_ARGUMENT(buf);

	// Always start off with a command.
	for (const auto &act : actions) {
		if (partialActionString == act.name.substr(0, partialActionString.length())) {
			addCompletionSuffix(autoComplete, "", 0, act.name.c_str(), true, false);
		}
	}

	// Add quit and exit too.
	if (partialActionString == std::string("exit").substr(0, partialActionString.length())) {
		addCompletionSuffix(autoComplete, "", 0, "exit", true, false);
	}
	if (partialActionString == std::string("quit").substr(0, partialActionString.length())) {
		addCompletionSuffix(autoComplete, "", 0, "quit", true, false);
	}
}

static void nodeCompletion(const std::string &buf, linenoiseCompletions *autoComplete, dv::ConfigAction action,
	const std::string &partialNodeString) {
	UNUSED_ARGUMENT(action);

	// If partialNodeString is still empty, the first thing is to complete the root.
	if (partialNodeString.empty()) {
		addCompletionSuffix(autoComplete, buf.c_str(), buf.length(), "/", false, false);
		return;
	}

	// Get all the children of the last fully defined node (/ or /../../).
	auto lastSlash = partialNodeString.rfind('/');
	if (lastSlash == std::string::npos) {
		// No / found, invalid, cannot auto-complete.
		return;
	}

	// Include slash character in size.
	lastSlash++;

	// Send request for all children names.
	auto nodeOffset = dataBufferSend.CreateString(partialNodeString.substr(0, lastSlash));

	auto msg = std::make_unique<dv::ConfigActionDataBuilder>(dataBufferSend);

	msg->add_action(dv::ConfigAction::GET_CHILDREN);
	msg->add_node(nodeOffset);

	try {
		sendMessage(std::move(msg));
	}
	catch (const boost::system::system_error &) {
		// Failed to contact remote host, no auto-completion!
		return;
	}

	const dv::ConfigActionDataFlatbuffer *resp = nullptr;

	try {
		resp = receiveMessage();
	}
	catch (const boost::system::system_error &) {
		// Failed to contact remote host, no auto-completion!
		return;
	}

	if (resp->action() == dv::ConfigAction::CFG_ERROR) {
		// Invalid request made, no auto-completion.
		return;
	}

	// At this point we made a valid request and got back a full response.
	const auto respStr = resp->value()->str();
	boost::tokenizer<boost::char_separator<char>> children(respStr, boost::char_separator<char>("|", nullptr));

	size_t lengthOfIncompletePart = (partialNodeString.length() - lastSlash);

	for (const auto &child : children) {
		if (partialNodeString.substr(lastSlash, lengthOfIncompletePart) == child.substr(0, lengthOfIncompletePart)) {
			addCompletionSuffix(
				autoComplete, buf.c_str(), buf.length() - lengthOfIncompletePart, child.c_str(), false, true);
		}
	}
}

static void keyCompletion(const std::string &buf, linenoiseCompletions *autoComplete, dv::ConfigAction action,
	const std::string &nodeString, const std::string &partialKeyString) {
	UNUSED_ARGUMENT(action);

	// Send request for all attribute names for this node.
	auto nodeOffset = dataBufferSend.CreateString(nodeString);

	auto msg = std::make_unique<dv::ConfigActionDataBuilder>(dataBufferSend);

	msg->add_action(dv::ConfigAction::GET_ATTRIBUTES);
	msg->add_node(nodeOffset);

	try {
		sendMessage(std::move(msg));
	}
	catch (const boost::system::system_error &) {
		// Failed to contact remote host, no auto-completion!
		return;
	}

	const dv::ConfigActionDataFlatbuffer *resp = nullptr;

	try {
		resp = receiveMessage();
	}
	catch (const boost::system::system_error &) {
		// Failed to contact remote host, no auto-completion!
		return;
	}

	if (resp->action() == dv::ConfigAction::CFG_ERROR) {
		// Invalid request made, no auto-completion.
		return;
	}

	// At this point we made a valid request and got back a full response.
	const auto respStr = resp->value()->str();
	boost::tokenizer<boost::char_separator<char>> attributes(respStr, boost::char_separator<char>("|", nullptr));

	for (const auto &attr : attributes) {
		if (partialKeyString == attr.substr(0, partialKeyString.length())) {
			addCompletionSuffix(
				autoComplete, buf.c_str(), buf.length() - partialKeyString.length(), attr.c_str(), true, false);
		}
	}
}

static void valueCompletion(const std::string &buf, linenoiseCompletions *autoComplete, dv::ConfigAction action,
	const std::string &nodeString, const std::string &keyString, const std::string &partialValueString) {
	UNUSED_ARGUMENT(action);

	// Send request for the current value, so we can auto-complete with it as default.
	auto nodeOffset = dataBufferSend.CreateString(nodeString);
	auto keyOffset  = dataBufferSend.CreateString(keyString);

	auto msg = std::make_unique<dv::ConfigActionDataBuilder>(dataBufferSend);

	msg->add_action(dv::ConfigAction::GET);
	msg->add_node(nodeOffset);
	msg->add_key(keyOffset);
	msg->add_type(static_cast<dv::ConfigType>(dv::ConfigType::UNKNOWN));

	try {
		sendMessage(std::move(msg));
	}
	catch (const boost::system::system_error &) {
		// Failed to contact remote host, no auto-completion!
		return;
	}

	const dv::ConfigActionDataFlatbuffer *resp = nullptr;

	try {
		resp = receiveMessage();
	}
	catch (const boost::system::system_error &) {
		// Failed to contact remote host, no auto-completion!
		return;
	}

	if (resp->action() == dv::ConfigAction::CFG_ERROR) {
		// Invalid request made, no auto-completion.
		return;
	}

	if (!partialValueString.empty()) {
		// If there already is content, we can't do any auto-completion here, as
		// we have no idea about what a valid value would be to complete ...
		// Unless this is a boolean, then we can propose true/false strings.
		if (static_cast<dv::Config::AttributeType>(resp->type()) == dv::Config::AttributeType::BOOL) {
			if (partialValueString == std::string("true").substr(0, partialValueString.length())) {
				addCompletionSuffix(
					autoComplete, buf.c_str(), buf.length() - partialValueString.length(), "true", false, false);
			}
			if (partialValueString == std::string("false").substr(0, partialValueString.length())) {
				addCompletionSuffix(
					autoComplete, buf.c_str(), buf.length() - partialValueString.length(), "false", false, false);
			}
		}

		return;
	}

	// At this point we made a valid request and got back a full response.
	// We can just use it directly and paste it in as completion.
	auto respString = resp->value()->str();

	if (static_cast<dv::Config::AttributeType>(resp->type()) == dv::Config::AttributeType::STRING) {
		// If string, it could be an empty string or contain spaces.
		// Add double quotes in that case to ease usage.
		if (respString.empty() || (respString.find(' ') != std::string::npos)) {
			respString = '"' + respString + '"';
		}
	}

	addCompletionSuffix(autoComplete, buf.c_str(), buf.length(), respString.c_str(), false, false);

	// If this is a boolean value, we can also add the inverse as a second completion.
	if (static_cast<dv::Config::AttributeType>(resp->type()) == dv::Config::AttributeType::BOOL) {
		if (resp->value()->string_view() == "true") {
			addCompletionSuffix(autoComplete, buf.c_str(), buf.length(), "false", false, false);
		}
		else {
			addCompletionSuffix(autoComplete, buf.c_str(), buf.length(), "true", false, false);
		}
	}
}

static void addCompletionSuffix(linenoiseCompletions *autoComplete, const char *buf, size_t completionPoint,
	const char *suffix, bool endSpace, bool endSlash) {
	std::string concat;

	if (endSpace) {
		if (endSlash) {
			concat = fmt::format(FMT_STRING("{:.{}s}{:s}/ "), buf, completionPoint, suffix);
		}
		else {
			concat = fmt::format(FMT_STRING("{:.{}s}{:s} "), buf, completionPoint, suffix);
		}
	}
	else {
		if (endSlash) {
			concat = fmt::format(FMT_STRING("{:.{}s}{:s}/"), buf, completionPoint, suffix);
		}
		else {
			concat = fmt::format(FMT_STRING("{:.{}s}{:s}"), buf, completionPoint, suffix);
		}
	}

	linenoiseAddCompletion(autoComplete, concat.c_str());
}
