#include "common_utils.h"

#include <algorithm>
#include <ios>
#include <stdio.h>
#include <sstream>
#include <tuple>

// NOTE: Only needed during testing
#include <functional>

#include <mysql.h>
#include <mysqld_error.h>
#include <string.h>
#include <string>
#include <unistd.h>

#include "proxysql_utils.h"
#include "SpookyV2.h"
#include "tap.h"
#include "utils.h"
#include "command_line.h"
#include "json.hpp"

using nlohmann::ordered_json;
using std::string;
using std::vector;

////////////////////////////////////////////////////////////////////////////////
//                         GENERIC HELPER FUNCTIONS                           //
////////////////////////////////////////////////////////////////////////////////

std::vector<std::string> str_split(const std::string& s, char delimiter) {
	std::vector<std::string> tokens {};
	std::string token {};
	std::istringstream tokenStream(s);

	while (std::getline(tokenStream, token, delimiter)) {
		tokens.push_back(token);
	}

	return tokens;
}

std::string replace(const std::string& s, const std::string& from, const std::string& to) {
	std::string res { s };

	if(!from.empty()) {
		for(size_t pos = 0; (pos = res.find(from, pos)) != std::string::npos; pos += to.size()) {
			res.replace(pos, from.size(), to);
		}
	}

	return res;
}

void parse_result_to_json(MYSQL_RES *result, ordered_json& j) {
	if(!result) { return; }

	// Get the fields
	std::vector<std::string> field_names {};

	mysql_fetch_fields(result);
	int num_fields = mysql_num_fields(result);
	MYSQL_FIELD* fields = mysql_fetch_fields(result);

	for(int i = 0; i < num_fields; i++) {
		field_names.push_back(fields[i].name);
	}

	// Get each of the row values
	MYSQL_ROW row = nullptr;

	while ((row = mysql_fetch_row(result))) {
		ordered_json j_row {};
		unsigned long *lengths;
		lengths = mysql_fetch_lengths(result);

		for(int i = 0; i < num_fields; i++) {
			if (row[i]) {
				j_row[field_names[i]] = row[i];
			} else {
				j_row[field_names[i]] = "NULL";
			}
		}

		j.push_back(j_row);
	}
}

bool json_server_comparator(const ordered_json& j_srv_1, const ordered_json& j_srv_2) {
	std::string id_1 {
		std::to_string(static_cast<int>(j_srv_1.at("hostgroup_id"))) +
		std::string { j_srv_1.at("hostname") } +
		std::to_string(static_cast<int>(j_srv_1.at("port")))
	};
	std::string id_2 {
		std::to_string(static_cast<int>(j_srv_2.at("hostgroup_id"))) +
		std::string { j_srv_2.at("hostname") } +
		std::to_string(static_cast<int>(j_srv_2.at("port")))
	};
	
	return id_1 < id_2;
}

////////////////////////////////////////////////////////////////////////////////
//                          HELPER ERROR FUNCTIONS                            //
////////////////////////////////////////////////////////////////////////////////

std::pair<int, std::string> create_query_error(
	MYSQL* conn, const std::string& query, const char* file, const int line
) {
	std::string t_err_msg { "File '%s', line '%d', Error: Query '%s' failed with error '%s'" };
	std::string err_msg {};
	string_format(t_err_msg, err_msg, file, line, query.c_str(), mysql_error(conn));

	return { EXIT_FAILURE, err_msg };
}

std::pair<int, nlohmann::ordered_json> internal_error(const std::string& err_msg, const char* file, const int line) {
	std::string t_err_msg { "File '%s', line '%d', Error: '%s'" };
	std::string f_err_msg {};

	string_format(t_err_msg, f_err_msg, file, line, err_msg.c_str());
	return { EXIT_FAILURE, { { "err_type", "internal_error" }, { "err_msg", f_err_msg } } };
}

std::pair<int, nlohmann::ordered_json> invalid_input_error(const std::string& err_msg, const char* file, const int line) {
	std::string t_err_msg { "File '%s', line '%d', Error: '%s'" };
	std::string f_err_msg {};

	string_format(t_err_msg, f_err_msg, file, line, err_msg.c_str());
	return { EXIT_FAILURE, { { "err_type", "invalid_input" }, { "err_msg", f_err_msg } } };
}

std::pair<int, nlohmann::ordered_json> invalid_config_error(const std::string& err_msg, const char* file, const int line) {
	std::string t_err_msg { "File '%s', line '%d', Error: '%s'" };
	std::string f_err_msg {};

	string_format(t_err_msg, f_err_msg, file, line, err_msg.c_str());
	return { EXIT_FAILURE, { { "err_type", "invalid_config" }, { "err_msg", f_err_msg } } };
}

std::pair<int, nlohmann::ordered_json> invalid_json_error(
	const std::string& err_msg,
	const char* file,
	const int line
) {
	std::string t_err_msg { "File '%s', line '%d', Error: '%s'" };
	std::string f_err_msg {};

	string_format(t_err_msg, f_err_msg, file, line, err_msg.c_str());
	return { EXIT_FAILURE, { { "err_type", "invalid_payload" }, { "err_msg", f_err_msg } } };
}

std::pair<int, std::string> serialize_verification_error(
	const nlohmann::ordered_json& j_verification_err,
	std::string& out_error_str
) {
	std::pair<int, std::string> result { EXIT_SUCCESS, "" };

	try {
		nlohmann::ordered_json c_verification_error = j_verification_err;

		nlohmann::ordered_json j_exp_proxysql_state =
			j_verification_err.at("exp_proxysql_state");
		nlohmann::ordered_json j_act_proxysql_state =
			j_verification_err.at("act_proxysql_state");

		std::sort(
			j_exp_proxysql_state.begin(),
			j_exp_proxysql_state.end(),
			json_server_comparator
		);

		std::sort(
			j_act_proxysql_state.begin(),
			j_act_proxysql_state.end(),
			json_server_comparator
		);

		c_verification_error["exp_proxysql_state"] = nlohmann::ordered_json::array();
		c_verification_error["act_proxysql_state"] = nlohmann::ordered_json::array();

		unsigned int elem_num = 0;

		for (std::size_t i = 0; i < j_exp_proxysql_state.size(); i++) {
			c_verification_error["exp_proxysql_state"].push_back(
				"%" + std::to_string(elem_num) + "s"
			);
			elem_num += 1;
		}

		for (std::size_t i = 0; i < j_act_proxysql_state.size(); i++) {
			c_verification_error["act_proxysql_state"].push_back(
				"%" + std::to_string(elem_num) + "s"
			);
			elem_num += 1;
		}

		elem_num = 0;

		std::string str_result { c_verification_error.dump(4) };

		for (const nlohmann::ordered_json& j_state_elem : j_exp_proxysql_state) {
			std::string elem_str { j_state_elem.dump() };

			elem_str = replace(elem_str, "{", "{ ");
			elem_str = replace(elem_str, "}", " }");

			str_result = replace(
				str_result,
				"\"%" + std::to_string(elem_num) + "s\"",
				elem_str
			);
			elem_num += 1;
		}

		for (const nlohmann::ordered_json& j_state_elem : j_act_proxysql_state) {
			std::string elem_str { j_state_elem.dump() };

			elem_str = replace(elem_str, "{", "{ ");
			elem_str = replace(elem_str, "}", " }");

			str_result = replace(
				str_result,
				"\"%" + std::to_string(elem_num) + "s\"",
				elem_str
			);
			elem_num += 1;
		}

		// add proper indentation
		str_result = replace(str_result, "\n", "\n        ");

		// fill the output parameter
		out_error_str = str_result;
	} catch (const std::exception& e) {
		result = {
			EXIT_FAILURE,
			"Malformed JSON 'verification_error': '" + std::string { e.what() } + "'"
		};
	}

	return result;
}

////////////////////////////////////////////////////////////////////////////////

std::pair<int, nlohmann::ordered_json> verification_error(
	const string& err_msg,
	const vector<server_status>& exp_cluster_state,
	const vector<server_status>& act_cluster_state,
	const cluster_state_changes& cluster_st_diff,
	const string& exp_state_timestamp,
	const string& act_state_timestamp
) {
	nlohmann::ordered_json j_exp_cluster_state =
		cluster_status_to_json(exp_cluster_state);
	nlohmann::ordered_json j_act_cluster_state =
		cluster_status_to_json(act_cluster_state);

	std::sort(
		j_exp_cluster_state.begin(),
		j_exp_cluster_state.end(),
		json_server_comparator
	);

	std::sort(
		j_act_cluster_state.begin(),
		j_act_cluster_state.end(),
		json_server_comparator
	);

	std::string exp_cluster_state_checksum =
		cluster_status_checksum(exp_cluster_state);
	std::string act_cluster_state_checksum =
		cluster_status_checksum(act_cluster_state);

	return {
		EXIT_FAILURE, 
		{
			{ "err_type", "verification_error" },
			{ "err_msg", err_msg },
			{ "cluster_state_diff", cluster_st_diff },
			{ "exp_proxysql_state_checksum", exp_cluster_state_checksum },
			{ "exp_proxysql_state", j_exp_cluster_state },
			{ "exp_state_timestamp", exp_state_timestamp },
			{ "act_proxysql_state_checksum", act_cluster_state_checksum },
			{ "act_proxysql_state", j_act_cluster_state },
			{ "act_state_timestamp", act_state_timestamp }
		}
	};
}

std::vector<std::string> get_invalid_keys(std::vector<std::string> valid_keys, json elem) {
	std::vector<std::string> invalid_keys {};

	std::vector<std::string> found_keys {};
	for (const auto& item : elem.items()) {
		found_keys.push_back(item.key());
	}

	for (const auto& key : found_keys) {
		bool invalid_key_found =
			std::find(
				valid_keys.begin(),
				valid_keys.end(),
				key
			) == std::end(valid_keys);

		if (invalid_key_found) {
			invalid_keys.push_back(key);
		}
	}

	return invalid_keys;
}

std::pair<int,std::string> gen_invalid_keys_err(
	const std::vector<std::string>& invalid_keys,
	const std::string name
) {
	if (!invalid_keys.empty()) {
		std::string t_err_msg { "'%s' contains invalid keys: [%s]" };
		std::string err_msg {};
		std::string invalid_keys_str { acc_keys(invalid_keys) };

		string_format(t_err_msg, err_msg, name.c_str(), invalid_keys_str.c_str());
		return { EXIT_FAILURE, err_msg };
	} else {
		return { EXIT_SUCCESS, "" };
	}
}

bool check_present_and_type(const json& j, const std::vector<std::string>& path, const json::value_t& type) {
	bool res = false;

	json cur_j {};

	for (const auto& step : path) {
		if (j.contains(step)) {
			cur_j = j.at(step);

			if (&step == &path.back()) {
				return type == cur_j.type();
			}
		} else {
			break;
		}
	}

	return res;
}

bool matching_server_status(const server_status& srv_st1, const server_status& srv_st2) {
	bool res = false;
	bool same_hid_host =
		std::get<0>(srv_st1) == std::get<0>(srv_st2) &&
		std::get<1>(srv_st1) == std::get<1>(srv_st2) &&
		std::get<2>(srv_st1) == std::get<2>(srv_st2);

	if (same_hid_host) {
		std::vector<std::string> allowed_sts {
			str_split(std::get<3>(srv_st1), '|')
		};

		if (std::find(allowed_sts.begin(), allowed_sts.end(), std::get<3>(srv_st2)) != allowed_sts.end()) {
			const int64_t exp_weight = std::get<MYSQL_SERVER_STATUS_T::WEIGHT>(srv_st1);
			const int64_t exp_max_conns = std::get<MYSQL_SERVER_STATUS_T::MAX_CONNS>(srv_st1);
			const int32_t exp_use_ssl = std::get<MYSQL_SERVER_STATUS_T::USE_SSL>(srv_st1);
			const std::string exp_comment = std::get<MYSQL_SERVER_STATUS_T::COMMENT>(srv_st1);

			bool match = true;

			if (exp_weight != -1) {
				match = match && exp_weight == std::get<MYSQL_SERVER_STATUS_T::WEIGHT>(srv_st2);
			}
			if (exp_max_conns != -1) {
				match = match && exp_max_conns == std::get<MYSQL_SERVER_STATUS_T::MAX_CONNS>(srv_st2);
			}
			if (exp_use_ssl != -1) {
				match = match && exp_use_ssl == std::get<MYSQL_SERVER_STATUS_T::USE_SSL>(srv_st2);
			}
			match = match && exp_comment == std::get<MYSQL_SERVER_STATUS_T::COMMENT>(srv_st2);

			res = match;
		}
	}

	return res;
}

bool check_cluster_status(const cluster_status& exp_status, const cluster_status& act_status) {
	// filter 'OFFLINE_HARD' servers due to the transitive nature of the state
	cluster_status f_act_status {};
	std::copy_if(
		act_status.begin(),
		act_status.end(),
		std::back_inserter(f_act_status),
		[] (const server_status& srv_st) -> bool {
			return std::get<3>(srv_st) != "OFFLINE_HARD";
		}
	);

	// check for different size
	if (exp_status.size() != f_act_status.size()) { return false; }

	// we first sort for hostgroup and for hostname, as toguether should form an id for sorting
	cluster_status c_exp_status { exp_status };
	cluster_status c_act_status { f_act_status };

	std::sort(
		c_exp_status.begin(),
		c_exp_status.end(),
		[] (const server_status& srv_st1, const server_status& srv_st2) -> bool {
			const std::string srv_st1_id { std::to_string(std::get<0>(srv_st1)) + std::get<1>(srv_st1) };
			const std::string srv_st2_id { std::to_string(std::get<0>(srv_st2)) + std::get<1>(srv_st2) };

			return srv_st1_id > srv_st2_id;
		}
	);

	std::sort(
		c_act_status.begin(),
		c_act_status.end(),
		[] (const server_status& srv_st1, const server_status& srv_st2) -> bool {
			const std::string srv_st1_id { std::to_string(std::get<0>(srv_st1)) + std::get<1>(srv_st1) };
			const std::string srv_st2_id { std::to_string(std::get<0>(srv_st2)) + std::get<1>(srv_st2) };

			return srv_st1_id > srv_st2_id;
		}
	);

	return std::equal(
		c_exp_status.begin(),
		c_exp_status.end(),
		c_act_status.begin(),
		[] (const server_status& srv_st1, const server_status& srv_st2) {
			return matching_server_status(srv_st1, srv_st2);
		}
	);
}

bool compare_mysql_servers(
	const std::vector<mysql_server_def>& servers_1,
	const std::vector<mysql_server_def>& servers_2
) {
	std::vector<mysql_server_def> c_servers_1 { servers_1 };
	std::vector<mysql_server_def> c_servers_2 { servers_2 };

	std::sort(
		c_servers_1.begin(),
		c_servers_1.end(),
		[] (const mysql_server_def& srv_st1, const mysql_server_def& srv_st2) -> bool {
			const std::string srv_st1_id { std::to_string(std::get<0>(srv_st1)) + std::get<1>(srv_st1) };
			const std::string srv_st2_id { std::to_string(std::get<0>(srv_st2)) + std::get<1>(srv_st2) };

			return srv_st1_id > srv_st2_id;
		}
	);

	std::sort(
		c_servers_2.begin(),
		c_servers_2.end(),
		[] (const mysql_server_def& srv_st1, const mysql_server_def& srv_st2) -> bool {
			const std::string srv_st1_id { std::to_string(std::get<0>(srv_st1)) + std::get<1>(srv_st1) };
			const std::string srv_st2_id { std::to_string(std::get<0>(srv_st2)) + std::get<1>(srv_st2) };

			return srv_st1_id > srv_st2_id;
		}
	);

	return c_servers_1 == c_servers_2;
}

std::pair<int, std::string> prepare_mysql_servers_config(
	MYSQL* proxysql_admin,
	const std::vector<mysql_server_def>& servers
) {
	int query_error = 0;

	// Remove the current configured servers
	query_error = mysql_query(proxysql_admin, "DELETE FROM mysql_servers");
	if (query_error) {
		return create_query_error(proxysql_admin, "DELETE FROM mysql_servers", __FILE__, __LINE__);
	}

	// Prevent any reconfiguration until all the cluster is setup
	const std::string load_to_runtime_query { "LOAD MYSQL SERVERS TO RUNTIME" };
	query_error = mysql_query(proxysql_admin, load_to_runtime_query.c_str());
	if (query_error) {
		return create_query_error(proxysql_admin, load_to_runtime_query, __FILE__, __LINE__);
	}

	// Leave sometime after "LOAD MYSQL SERVERS TO RUNTIME" for leaving time
	// to ProxySQL to reconfigure servers, otherwise query might fail.
	usleep(500 * 1000);

	std::string query { "INSERT INTO mysql_servers(hostgroup_id,hostname,port, status, max_replication_lag,comment) VALUES" };

	for (const auto& server : servers) {
		std::string t_server_values {};
		std::string server_values {};

		if (&server != &servers.back()) {
			t_server_values = " (%d,'%s',%d,'%s',%d,'%s'), ";
		} else {
			t_server_values = " (%d,'%s',%d,'%s',%d,'%s')";
		}

		string_format(
			t_server_values,
			server_values,
			std::get<0>(server),
			std::get<1>(server).c_str(),
			std::get<2>(server),
			std::get<3>(server).c_str(),
			std::get<4>(server),
			std::get<5>(server).c_str()
		);

		query += server_values;
	}

	query_error = mysql_query(proxysql_admin, query.c_str());
	if (query_error) {
		return create_query_error(proxysql_admin, query, __FILE__, __LINE__);
	}

	return { EXIT_SUCCESS, "" };
}

std::pair<int, std::string> prepare_mysql_hostgroup_attributes_config(
	MYSQL* proxysql_admin, const std::vector<hostgroup_attributes_def>& hostgroup_attributes
) {
	int query_error = 0;

	query_error = mysql_query(proxysql_admin, "DELETE FROM mysql_hostgroup_attributes");
	if (query_error) {
		return create_query_error(proxysql_admin, "DELETE FROM mysql_hostgroup_attributes", __FILE__, __LINE__);
	}

	std::string query { "INSERT INTO mysql_hostgroup_attributes(hostgroup_id,servers_defaults,hostgroup_settings) VALUES" };

	for (const auto& hg_attrib : hostgroup_attributes) {
		std::string t_values {};
		std::string values {};

		if (&hg_attrib != &hostgroup_attributes.back()) {
			t_values = " (%d,'%s','%s'), ";
		} else {
			t_values = " (%d,'%s','%s')";
		}

		ordered_json j_servers_defaults {};

		int64_t def_weight = std::get<HOSTGROUP_ATTRIBUTES_T::WEIGHT>(hg_attrib);
		int64_t def_max_conns = std::get<HOSTGROUP_ATTRIBUTES_T::MAX_CONNS>(hg_attrib);
		int64_t def_use_ssl = std::get<HOSTGROUP_ATTRIBUTES_T::USE_SSL>(hg_attrib);

		if (def_weight != -1) {
			j_servers_defaults["weight"] = def_weight;
		}
		if (def_max_conns != -1) {
			j_servers_defaults["max_connections"] = def_max_conns;
		}
		if (def_use_ssl != -1) {
			j_servers_defaults["use_ssl"] = def_use_ssl;
		}

		const string s_servers_defaults { j_servers_defaults.dump() };

		ordered_json j_hostgroup_settings {};
		int32_t monitor_slave_lag_when_null = std::get<HOSTGROUP_ATTRIBUTES_T::MONITOR_SLAVE_LAG_WHEN_NULL>(hg_attrib);
		if (monitor_slave_lag_when_null != -1) {
			j_hostgroup_settings["monitor_slave_lag_when_null"] = monitor_slave_lag_when_null;
		}

		const string s_hostgroup_settings { j_hostgroup_settings.dump() };

		string_format(
			t_values,
			values,
			std::get<HOSTGROUP_ATTRIBUTES_T::HG_ID>(hg_attrib),
			s_servers_defaults.c_str(),
			s_hostgroup_settings.c_str()
		);

		query += values;
	}

	query_error = mysql_query(proxysql_admin, query.c_str());
	if (query_error) {
		return create_query_error(proxysql_admin, query, __FILE__, __LINE__);
	}

	return { EXIT_SUCCESS, "" };
}

std::pair<int, std::string> extract_monitor_config(
	const json& test_def, std::vector<monitor_variable>& out_monitor_variables
) {
	std::vector<monitor_variable> result {};

	// perform basic payload checks
	if (!test_def.is_object()) {
		return { EXIT_FAILURE, "Invalid input. Expected 'test_definition' should be a JSON object." };
	}

	bool has_monitor_config =
		check_present_and_type(test_def, {"mysql_monitor_config"}, json::value_t::array);
	if (has_monitor_config == false) {
		return { EXIT_SUCCESS, "No 'mysql_monitor_config' provided, using default values." };
	}

	json m_monitor_variables = test_def["mysql_monitor_config"];

	for (const auto& m_monitor_variable : m_monitor_variables.items()) {
		if (
			m_monitor_variable.value().type() != json::value_t::object ||
			m_monitor_variable.value().size() != 1
		) {
			return {
				EXIT_FAILURE,
				"Object of kind '{ var_name: var_value }' expected for"
				" 'mysql_monitor_config' variable but got: '" +
				m_monitor_variable.value().dump() + "'"
			};
		}

		for (const auto& var_value : m_monitor_variable.value().items()) {
			if (
				var_value.value().type() != json::value_t::number_unsigned &&
				var_value.value().type() != json::value_t::number_integer
			) {
				return {
					EXIT_FAILURE,
					"Type expected for 'mysql_monitor_config' variable '" + var_value.value().dump() +
					"' was either: 'number_unsigned' or 'number_integer'"
				};
			}
			result.push_back({ var_value.key(), var_value.value() });
		}
	}

	// return the extracted values
	out_monitor_variables =  result;

	return { EXIT_SUCCESS, "" };
}

std::pair<int, std::string> set_monitor_variables(
	MYSQL* proxysql_admin,
	const std::vector<std::string>& allowed_variables,
	const std::vector<monitor_variable>& monitor_variables
) {
	std::string t_set_var_query {
		"SET mysql-%s=%d"
	};
	for (const auto& monitor_variable : monitor_variables) {
		const auto allowed =
			std::find(
				allowed_variables.begin(),
				allowed_variables.end(),
				monitor_variable.first
			);

		if (allowed != std::end(allowed_variables)) {
			std::string set_var_query {};
			string_format(
				t_set_var_query, set_var_query, monitor_variable.first.c_str(),
				monitor_variable.second
			);

			int query_err = mysql_query(proxysql_admin, set_var_query.c_str());
			if (query_err) {
				return {
					EXIT_FAILURE,
					"Setting monitor variable query '" + set_var_query + "'" +
					" failed with error: '" + std::string { mysql_error(proxysql_admin) }
				};
			}
		}
	}

	return { EXIT_SUCCESS, "" };
}

std::vector<monitor_variable> set_monitor_variables_defaults(
	const std::vector<monitor_variable>& monitor_variables,
	const std::vector<monitor_variable>& def_vars_values
) {
	std::vector<monitor_variable> result { monitor_variables };

	for (auto& def_var : def_vars_values) {
		auto found_var = std::find_if(
			result.begin(),
			result.end(),
			[&def_var] (const monitor_variable& var) {
				return def_var.first == var.first;
			}
		);

		if (found_var == std::end(result)) {
			result.push_back(def_var);
		}
	}

	return result;
}

const std::vector<std::string> valid_mysql_entries {
	"hostgroup_id",
	"hostname",
	"port",
	"status",
	"max_replication_lag",
	"comment"
};

std::pair<int,std::string> extract_mysql_servers(
	const json& galera_test_def,
	std::vector<mysql_server_def>& out_mysql_servers
) {
	// result
	std::vector<mysql_server_def> result {};

	// perform basic payload checks
	if (!galera_test_def.is_object()) {
		return { EXIT_FAILURE, "Invalid input. Expected 'test_definition' should be a JSON object." };
	}

	bool has_mysql_servers =
		check_present_and_type(galera_test_def, {"mysql_servers"}, json::value_t::array);
	if (has_mysql_servers == false) {
		return { EXIT_FAILURE, "Invalid input. Unable to find required field 'mysql_servers'" };
	}

	json m_mysql_servers = galera_test_def["mysql_servers"];
	if (!m_mysql_servers.is_array()) {
		return { EXIT_FAILURE, "Invalid input. 'mysql_servers' isn't of expected type 'array'" };
	}

	for (const auto& m_mysql_server : m_mysql_servers) {
		// *********************** CHECK FOR INVALID KEYS ******************* //

		std::vector<std::string> invalid_keys {
			get_invalid_keys(valid_mysql_entries, m_mysql_server)
		};

		if (!invalid_keys.empty()) {
			return gen_invalid_keys_err(invalid_keys, "mysql_servers");
		}

		// ****************************************************************** //

		int hostgroup_id;
		std::string hostname {};
		int port;
		std::string status { "ONLINE" };
		int max_replication_lag = 0;
		std::string comment {};

		try {
			hostgroup_id = m_mysql_server.at("hostgroup_id");
			hostname = m_mysql_server.at("hostname");
			port = m_mysql_server.at("port");

			if (m_mysql_server.contains("max_replication_lag")) {
				max_replication_lag = m_mysql_server.at("max_replication_lag");
			}

			bool has_status = m_mysql_server.contains("status");
			if (has_status) {
				status = m_mysql_server.at("status");
			}

			if (m_mysql_server.contains("comment")) {
				comment = m_mysql_server.at("comment");
			}
		} catch(const std::exception& e) {
			return { EXIT_FAILURE, e.what() };
		}

		result.push_back(std::make_tuple(hostgroup_id, hostname, port, status, max_replication_lag, comment));
	}

	// return the extracted values
	out_mysql_servers =  std::move(result);

	return { EXIT_SUCCESS, "" };
}

const vector<string> valid_hostgroup_attributes_entries {
	"hostgroup_id", "weight", "max_connections", "use_ssl", "monitor_slave_lag_when_null"
};

std::pair<int,std::string> extract_mysql_hostgroup_attributes(
	const json& test_def, std::vector<hostgroup_attributes_def>& out_hostgroup_attributes
) {
	std::pair<int, std::string> err_res {};
	std::vector<hostgroup_attributes_def> result {};

	if (!test_def.is_object()) {
		return { EXIT_FAILURE, "Invalid input. Expected 'test_definition' should be a JSON object." };
	}

	// 'mysql_hostgroup_attributes' is an optional field, exit if not found
	bool has_server_attrs = check_present_and_type(test_def, {"mysql_hostgroup_attributes"}, json::value_t::array);
	if (!has_server_attrs) {
		return { EXIT_SUCCESS, "" };
	}

	json j_hostgroups_attrs = test_def["mysql_hostgroup_attributes"];
	if (!j_hostgroups_attrs.is_array()) {
		return { EXIT_FAILURE, "Invalid input. 'mysql_hostgroup_attributes' isn't of expected type 'array'" };
	}

	for (const auto& j_hg_attrs : j_hostgroups_attrs) {
		// *********************** CHECK FOR INVALID KEYS ******************* //

		std::vector<std::string> invalid_keys { get_invalid_keys(valid_hostgroup_attributes_entries, j_hg_attrs) };

		if (!invalid_keys.empty()) {
			return gen_invalid_keys_err(invalid_keys, "mysql_hostgroup_attributes");
		}

		// ****************************************************************** //

		uint64_t hostgroup_id = 0;
		int64_t weight = -1;
		int64_t max_conns = -1;
		int32_t use_ssl = -1;
		int32_t monitor_slave_lag_when_null = -1;

		try {
			hostgroup_id = j_hg_attrs.at("hostgroup_id");

			const bool found_weight = j_hg_attrs.find("weight") != j_hg_attrs.end();
			const bool found_max_conns = j_hg_attrs.find("max_connections") != j_hg_attrs.end();
			const bool found_use_ssl = j_hg_attrs.find("use_ssl") != j_hg_attrs.end();
			const bool found_monitor_slave_lag_when_null = j_hg_attrs.find("monitor_slave_lag_when_null") != j_hg_attrs.end();

			weight = found_weight ? static_cast<int64_t>(j_hg_attrs.at("weight")) : -1;
			max_conns = found_max_conns ? static_cast<int64_t>(j_hg_attrs.at("max_connections")) : -1;
			use_ssl = found_use_ssl ? static_cast<int32_t>(j_hg_attrs.at("use_ssl")) : -1;
			monitor_slave_lag_when_null = found_monitor_slave_lag_when_null ? 
										  static_cast<int32_t>(j_hg_attrs.at("monitor_slave_lag_when_null")) : -1;
		} catch(const std::exception& e) {
			return { EXIT_FAILURE, e.what() };
		}

		result.push_back(std::make_tuple(hostgroup_id, weight, max_conns, use_ssl, monitor_slave_lag_when_null));
	}

	out_hostgroup_attributes = std::move(result);

	return { EXIT_SUCCESS, "" };
}

std::pair<int, std::string> get_current_mysql_servers(
	MYSQL* proxysql_admin,
	std::vector<mysql_server_def>& out_cur_mysql_servers
) {
	std::pair<int, std::string> err_res {};
	std::string t_err_msg { "'get_current_mysql_servers' failed with error: '%s'" };
	std::string err_msg {};

	const std::string mysql_servers_query { "SELECT hostgroup_id, hostname, port, max_replication_lag FROM mysql_servers" };
	int q_res = mysql_query(proxysql_admin, mysql_servers_query.c_str());

	if (q_res == 0) {
		ordered_json j_servers {};
		j_servers["mysql_servers"] = {};

		MYSQL_RES* my_servers_res = mysql_store_result(proxysql_admin);
		parse_result_to_json(my_servers_res, j_servers["mysql_servers"]);

		// convert the fields into the proper types
		for (ordered_json& j_server : j_servers["mysql_servers"]) {
			j_server["hostgroup_id"] = atoi(std::string {j_server["hostgroup_id"]}.c_str());
			j_server["port"] = atoi(std::string {j_server["port"]}.c_str());
			j_server["max_replication_lag"] = atoi(std::string {j_server["max_replication_lag"]}.c_str());
		}

		std::vector<mysql_server_def> res_cur_mysql_servers {};
		std::pair<int, std::string> ext_res =
			extract_mysql_servers(j_servers, res_cur_mysql_servers);

		if (ext_res.first == EXIT_SUCCESS) {
			out_cur_mysql_servers = res_cur_mysql_servers;
		} else {
			string_format(t_err_msg, err_msg, ext_res.second.c_str());
			err_res = { EXIT_FAILURE, err_msg };
		}
	} else {
		string_format(t_err_msg, err_msg, mysql_error(proxysql_admin));
		err_res = { EXIT_FAILURE, err_msg };
	}

	return err_res;
}

const std::vector<std::string> valid_server_status_entries {
	"hostgroup_id",
	"hostname",
	"port",
	"status",
	"weight",
	"max_connections",
	"use_ssl",
	"comment"
};

std::pair<int, std::string> extract_cluster_status(
	cluster_state state,
	const json& galera_test_def,
	std::vector<server_status>& out_cluster_status
) {
	// result
	std::vector<server_status> cluster_status {};

	// perform basic payload checks
	if (!galera_test_def.is_object()) {
		return {
			EXIT_FAILURE,
			"Invalid input. Expected 'test_definition' should be a JSON object."
		};
	}

	std::string json_key {};

	if (state == cluster_state::init_state) {
		json_key = "proxysql_init_state";
	} else {
		json_key = "proxysql_final_state";
	}

	bool has_proxysql_init_state =
		check_present_and_type(
			galera_test_def, { json_key }, json::value_t::object
		);

	if (has_proxysql_init_state) {
		std::string t_err_msg {
			"Invalid input. Unable to find required field '%s'"
		};
		std::string err_msg {};
		string_format(t_err_msg, err_msg, json_key.c_str());

		return {
			EXIT_FAILURE,
			err_msg
		};
	}

	ordered_json m_proxysql_init_state = galera_test_def[json_key];
	if (!m_proxysql_init_state.is_array()) {
		std::string t_err_msg {
			"Invalid input. '%s' isn't of expected type 'array'"
		};
		std::string err_msg {};
		string_format(t_err_msg, err_msg, json_key.c_str());

		return {
			EXIT_FAILURE,
			err_msg
		};
	}

	for (const auto& m_mysql_server : m_proxysql_init_state) {
		// *********************** CHECK FOR INVALID KEYS ******************* //

		std::vector<std::string> invalid_keys {
			get_invalid_keys(valid_server_status_entries, m_mysql_server)
		};

		if (!invalid_keys.empty()) {
			return gen_invalid_keys_err(invalid_keys, json_key);
		}

		// ****************************************************************** //

		int hostgroup_id;
		std::string hostname {};
		int port;
		std::string status {};
		int64_t weight = -1;
		int64_t max_conns = -1;
		int32_t use_ssl = -1;
		std::string comment {};

		try {
			hostgroup_id = m_mysql_server.at("hostgroup_id");
			hostname = m_mysql_server.at("hostname");
			port = m_mysql_server.at("port");
			status = m_mysql_server.at("status");

			if (m_mysql_server.contains("weight")) {
				weight = m_mysql_server.at("weight");
			}
			if (m_mysql_server.contains("max_connections")) {
				max_conns = m_mysql_server.at("max_connections");
			}
			if (m_mysql_server.contains("use_ssl")) {
				use_ssl = m_mysql_server.at("use_ssl");
			}
			if (m_mysql_server.contains("comment")) {
				comment = m_mysql_server.at("comment");
			}
		} catch(const std::exception& e) {
			return { EXIT_FAILURE, e.what() };
		}

		cluster_status.push_back(
			std::make_tuple(hostgroup_id, hostname, port, status, weight, max_conns, use_ssl, comment)
		);
	}

	// fill output parameter
	out_cluster_status = std::move(cluster_status);

	return { EXIT_SUCCESS, "" };
}

std::pair<int, std::string> get_current_cluster_status(
	MYSQL* proxysql_admin,
	std::vector<server_status>& out_cluster_status
) {
	std::pair<int, std::string> err_res {};
	std::string t_err_msg { "'get_current_cluster_status' failed with error: '%s'" };
	std::string err_msg {};

	const std::string cluster_status_query {
		"SELECT hostgroup_id,hostname,port,status,weight,max_connections,use_ssl,comment FROM runtime_mysql_servers"
	};
	int q_res = mysql_query(proxysql_admin, cluster_status_query.c_str());

	if (q_res == 0) {
		ordered_json j_servers {};
		j_servers["proxysql_init_state"] = {};

		MYSQL_RES* my_servers_res = mysql_store_result(proxysql_admin);
		parse_result_to_json(my_servers_res, j_servers["proxysql_init_state"]);
		mysql_free_result(my_servers_res);

		// convert the fields into the proper types
		for (ordered_json& j_server : j_servers["proxysql_init_state"]) {
			j_server["hostgroup_id"] = atoi(std::string { j_server["hostgroup_id"] }.c_str());
			j_server["hostname"] = std::string { j_server["hostname"] };
			j_server["port"] = atoi(std::string { j_server["port"] }.c_str());
			j_server["status"] = std::string { j_server["status"] };
			j_server["weight"] = std::stoi(std::string { j_server["weight"] });
			j_server["max_connections"] = std::stoi(std::string { j_server["max_connections"] });
			j_server["use_ssl"] = std::stoi(std::string { j_server["use_ssl"] });
			j_server["comment"] = std::string { j_server["comment"] };
		}

		std::vector<server_status> res_cluster_status {};

		auto ext_st_err = extract_cluster_status(
			cluster_state::init_state, j_servers, res_cluster_status
		);

		if (ext_st_err.first == EXIT_SUCCESS) {
			out_cluster_status = std::move(res_cluster_status);
		} else {
			string_format(t_err_msg, err_msg, ext_st_err.second.c_str());
			err_res = { EXIT_FAILURE, err_msg };
		}
	} else {
		string_format(t_err_msg, err_msg, mysql_error(proxysql_admin));
		err_res = { EXIT_FAILURE, err_msg };
	}

	return err_res;
}

ordered_json cluster_status_to_json(const std::vector<server_status>& cluster_status) {
	ordered_json result = ordered_json::array({});

	for (const auto& server_status : cluster_status) {
		ordered_json srv_result {
			{ "hostgroup_id", std::get<0>(server_status) },
			{ "hostname", std::get<1>(server_status) },
			{ "port", std::get<2>(server_status) },
			{ "status", std::get<3>(server_status) },
		};

		int64_t weight = std::get<MYSQL_SERVER_STATUS_T::WEIGHT>(server_status);
		int64_t max_conns = std::get<MYSQL_SERVER_STATUS_T::MAX_CONNS>(server_status);
		int64_t use_ssl = std::get<MYSQL_SERVER_STATUS_T::USE_SSL>(server_status);
		string comment = std::get<MYSQL_SERVER_STATUS_T::COMMENT>(server_status);

		if (weight != -1) {
			srv_result["weight"] = weight;
		}
		if (max_conns != -1) {
			srv_result["max_connections"] = max_conns;
		}
		if (use_ssl != -1) {
			srv_result["use_ssl"] = use_ssl;
		}

		result.push_back(srv_result);
	}

	return result;
}

std::string cluster_status_checksum(const std::vector<server_status>& cluster_status) {
	SpookyHash spooky {};
	spooky.Init(13, 4);

	for (const auto& srv_st : cluster_status) {
		std::string hostgroup_id { std::to_string(std::get<0>(srv_st)) };
		std::string hostname { std::get<1>(srv_st)  };
		std::string port { std::to_string(std::get<2>(srv_st)) };
		std::string status { std::get<3>(srv_st) };
		std::string weight { std::to_string(std::get<4>(srv_st)) };
		std::string max_conns { std::to_string(std::get<5>(srv_st)) };
		std::string use_ssl { std::to_string(std::get<6>(srv_st)) };
		std::string comment { std::get<7>(srv_st) };

		spooky.Update(hostgroup_id.c_str(), hostgroup_id.size());
		spooky.Update(hostname.c_str(), hostname.size());
		spooky.Update(port.c_str(), port.size());
		spooky.Update(status.c_str(), status.size());
		spooky.Update(weight.c_str(), weight.size());
		spooky.Update(max_conns.c_str(), max_conns.size());
		spooky.Update(use_ssl.c_str(), use_ssl.size());
		spooky.Update(comment.c_str(), comment.size());
	}

	uint64_t hash1, hash2;
	spooky.Final(&hash1, &hash2);

	std::stringstream sstream;
	sstream << std::hex << hash1;
	std::string res_hash { sstream.str() };

	return res_hash;
}

std::pair<int, std::string> serialize_result(const nlohmann::ordered_json& j_res, std::string& out_str_res) {
	nlohmann::ordered_json c_j_res = j_res;
	nlohmann::ordered_json j_proxysql_init_state {};
	nlohmann::ordered_json j_proxysql_final_state {};
	nlohmann::ordered_json j_mysql_servers {};
	nlohmann::ordered_json j_mysql_monitor_variables {};

	const auto j_comparator =
		[] (const nlohmann::ordered_json& j_srv_st_1, const nlohmann::ordered_json& j_srv_st_2) {
			std::string id_1 {
				std::to_string(static_cast<int>(j_srv_st_1.at("hostgroup_id"))) +
				std::string { j_srv_st_1.at("hostname") } +
				std::to_string(static_cast<int>(j_srv_st_1.at("port")))
			};
			std::string id_2 {
				std::to_string(static_cast<int>(j_srv_st_2.at("hostgroup_id"))) +
				std::string { j_srv_st_2.at("hostname") } +
				std::to_string(static_cast<int>(j_srv_st_2.at("port")))
			};

			return id_1 < id_2;
		};

	try {
		j_proxysql_init_state = j_res.at("proxysql_init_state");
		j_proxysql_final_state = j_res.at("proxysql_final_state");
		j_mysql_servers = j_res.at("mysql_servers");

		if (j_res.find("mysql_monitor_config") != j_res.end()) {
			j_mysql_monitor_variables = j_res.at("mysql_monitor_config");
		} else {
			j_mysql_monitor_variables = {};
		}

		std::sort(j_proxysql_init_state.begin(), j_proxysql_init_state.end(), j_comparator);
		std::sort(j_proxysql_final_state.begin(), j_proxysql_final_state.end(), j_comparator);
		std::sort(j_mysql_servers.begin(), j_mysql_servers.end(), j_comparator);

		c_j_res["proxysql_init_state"] = nlohmann::ordered_json::array();
		c_j_res["proxysql_final_state"] = nlohmann::ordered_json::array();
		c_j_res["mysql_servers"] = nlohmann::ordered_json::array();
		c_j_res["mysql_monitor_config"] = nlohmann::ordered_json::array();

		unsigned int elem_num = 0;

		for (std::size_t i = 0; i < j_mysql_servers.size(); i++) {
			c_j_res["mysql_servers"].push_back("%" + std::to_string(elem_num) + "s");
			elem_num += 1;
		}

		for (std::size_t i = 0; i < j_proxysql_init_state.size(); i++) {
			c_j_res["proxysql_init_state"].push_back(
				"%" + std::to_string(elem_num) + "s"
			);
			elem_num += 1;
		}

		for (std::size_t i = 0; i < j_proxysql_final_state.size(); i++) {
			c_j_res["proxysql_final_state"].push_back(
				"%" + std::to_string(elem_num) + "s"
			);
			elem_num += 1;
		}

		for (std::size_t i = 0; i < j_mysql_monitor_variables.size(); i++) {
			c_j_res["mysql_monitor_config"].push_back("%" + std::to_string(elem_num) + "s");
			elem_num += 1;
		}

		elem_num = 0;

		std::string str_result { c_j_res.dump(4) };

		for (const nlohmann::ordered_json& j_mysql_srv_elem : j_mysql_servers) {
			std::string elem_str { j_mysql_srv_elem.dump() };

			elem_str = replace(elem_str, "{", "{ ");
			elem_str = replace(elem_str, "}", " }");

			str_result = replace(
				str_result,
				"\"%" + std::to_string(elem_num) + "s\"",
				elem_str
			);
			elem_num += 1;
		}

		for (const nlohmann::ordered_json& j_state_elem : j_proxysql_init_state) {
			std::string elem_str { j_state_elem.dump() };

			elem_str = replace(elem_str, "{", "{ ");
			elem_str = replace(elem_str, "}", " }");

			str_result = replace(
				str_result,
				"\"%" + std::to_string(elem_num) + "s\"",
				elem_str
			);
			elem_num += 1;
		}

		for (const nlohmann::ordered_json& j_state_elem : j_proxysql_final_state) {
			std::string elem_str { j_state_elem.dump() };

			elem_str = replace(elem_str, "{", "{ ");
			elem_str = replace(elem_str, "}", " }");

			str_result = replace(
				str_result,
				"\"%" + std::to_string(elem_num) + "s\"",
				elem_str
			);
			elem_num += 1;
		}

		for (const nlohmann::ordered_json& j_state_elem : j_mysql_monitor_variables) {
			std::string elem_str { j_state_elem.dump() };

			elem_str = replace(elem_str, "{", "{ ");
			elem_str = replace(elem_str, "}", " }");

			str_result = replace(
				str_result,
				"\"%" + std::to_string(elem_num) + "s\"",
				elem_str
			);
			elem_num += 1;
		}

		// add proper indentation
		str_result = replace(str_result, "\n", "\n        ");

		// fill the output parameter
		out_str_res = str_result;
	} catch (const std::exception& e) {
		return { EXIT_FAILURE, e.what() };
	}

	return { EXIT_SUCCESS, "" };
}

std::string serialize_result(const nlohmann::ordered_json& j_result) {
	nlohmann::ordered_json c_j_result = j_result;

	// report all the simuation outputs
	unsigned int res_num = 0;

	std::vector<std::string> sim_results_str {};
	if (c_j_result.contains("results")) {
		for (auto& j_sim_result : c_j_result["results"]) {
			if (j_sim_result.contains("result")) {
				std::string sim_result_str {};
				const auto ser_res_err =
					serialize_result(j_sim_result["result"], sim_result_str);

				if (ser_res_err.first) {
					c_j_result = internal_error(ser_res_err.second, __FILE__, __LINE__);
					break;
				} else {
					sim_results_str.push_back(sim_result_str);
					j_sim_result = "%" + std::to_string(res_num) + "s";
				}
			} else if (
				j_sim_result.contains("err_type") &&
				(j_sim_result.at("err_type") == "verification_error" )
			) {
				std::string sim_err_str {};
				const auto ser_res_err =
					serialize_verification_error(j_sim_result, sim_err_str);

				if (ser_res_err.first) {
					c_j_result = internal_error(ser_res_err.second, __FILE__, __LINE__);
					break;
				} else {
					sim_results_str.push_back(sim_err_str);
					j_sim_result = "%" + std::to_string(res_num) + "s";
				}
			} else {
				std::string sim_result_str { j_sim_result.dump(4) };

				// add proper indentation
				sim_result_str = replace(sim_result_str, "\n", "\n        ");

				sim_results_str.push_back(sim_result_str);
				j_sim_result = "%" + std::to_string(res_num) + "s";
			}

			// update the placeholder
			res_num += 1;
		}
	}

	res_num = 0;

	std::string t_str_res { c_j_result.dump(4) };

	// replace the placeholders
	for (const auto& sim_str : sim_results_str) {
		t_str_res = replace(
			t_str_res,
			"\"%" + std::to_string(res_num) + "s\"",
			sim_str
		);
		res_num += 1;
	}

	return t_str_res;
}

string acc_keys(const vector<string>& keys) {
	const auto append = [](const string& a, const string& b) -> string {
		string f { a.size() > 0 ? string { a + "," } : a };
		return f + string { "\"" + b + "\"" };
	};
	return std::accumulate(keys.begin(), keys.end(), string {}, append);
}

std::string get_fmt_time() {
	time_t __timer;
	char __buffer[30];

	struct tm __tm_info {};
	time(&__timer);
	localtime_r(&__timer, &__tm_info);
	strftime(__buffer, 25, "%Y-%m-%d %H:%M:%S", &__tm_info);

	return std::string(__buffer);
}
