/*
 * CoordinatorsCommand.actor.cpp
 *
 * This source file is part of the FoundationDB open source project
 *
 * Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "boost/algorithm/string.hpp"

#include "fdbcli/fdbcli.actor.h"

#include "fdbclient/FDBOptions.g.h"
#include "fdbclient/IClientApi.h"
#include "fdbclient/Knobs.h"
#include "fdbclient/Schemas.h"
#include "fdbclient/ManagementAPI.actor.h"

#include "flow/Arena.h"
#include "flow/FastRef.h"
#include "flow/ThreadHelper.actor.h"
#include "flow/actorcompiler.h" // This must be the last #include.

namespace {

ACTOR Future<Void> printCoordinatorsInfo(Reference<IDatabase> db) {
	state Reference<ITransaction> tr = db->createTransaction();
	loop {
		try {
			// Hold the reference to the standalone's memory
			state ThreadFuture<Optional<Value>> descriptionF = tr->get(fdb_cli::clusterDescriptionSpecialKey);
			Optional<Value> description = wait(safeThreadFutureToFuture(descriptionF));
			ASSERT(description.present());
			printf("Cluster description: %s\n", description.get().toString().c_str());
			// Hold the reference to the standalone's memory
			state ThreadFuture<Optional<Value>> processesF = tr->get(fdb_cli::coordinatorsProcessSpecialKey);
			Optional<Value> processes = wait(safeThreadFutureToFuture(processesF));
			ASSERT(processes.present());
			std::vector<std::string> process_addresses;
			boost::split(process_addresses, processes.get().toString(), [](char c) { return c == ','; });
			printf("Cluster coordinators (%zu): %s\n", process_addresses.size(), processes.get().toString().c_str());
			printf("Type `help coordinators' to learn how to change this information.\n");
			return Void();
		} catch (Error& e) {
			wait(safeThreadFutureToFuture(tr->onError(e)));
		}
	}
}

ACTOR Future<bool> changeCoordinators(Reference<IDatabase> db, std::vector<StringRef> tokens) {
	state int retries = 0;
	state int notEnoughMachineResults = 0;
	state StringRef new_cluster_description;
	state std::string auto_coordinators_str;
	state bool disableConfigDB = false;
	StringRef nameTokenBegin = "description="_sr;
	StringRef noConfigDB = "--no-config-db"_sr;
	for (auto tok = tokens.begin() + 1; tok != tokens.end(); ++tok) {
		if (tok->startsWith(nameTokenBegin) && new_cluster_description.empty()) {
			new_cluster_description = tok->substr(nameTokenBegin.size());
			auto next = tok - 1;
			std::copy(tok + 1, tokens.end(), tok);
			tokens.resize(tokens.size() - 1);
			tok = next;
		} else if (tok->startsWith(noConfigDB)) {
			disableConfigDB = true;
			auto next = tok - 1;
			std::copy(tok + 1, tokens.end(), tok);
			tokens.resize(tokens.size() - 1);
			tok = next;
		}
	}

	state bool automatic = tokens.size() == 2 && tokens[1] == "auto"_sr;
	state Reference<ITransaction> tr = db->createTransaction();
	loop {
		tr->setOption(FDBTransactionOptions::SPECIAL_KEY_SPACE_ENABLE_WRITES);
		try {
			// update cluster description
			if (new_cluster_description.size()) {
				tr->set(fdb_cli::clusterDescriptionSpecialKey, new_cluster_description);
			}
			if (disableConfigDB) {
				// All that matters is the key is set.
				tr->set(fdb_cli::configDBSpecialKey, ""_sr);
			}
			// if auto change, read the special key to retrieve the recommended config
			if (automatic) {
				// if previous read failed, retry, otherwise, use the same recommened config
				if (!auto_coordinators_str.size()) {
					// Hold the reference to the standalone's memory
					state ThreadFuture<Optional<Value>> auto_coordinatorsF =
					    tr->get(fdb_cli::coordinatorsAutoSpecialKey);
					Optional<Value> auto_coordinators = wait(safeThreadFutureToFuture(auto_coordinatorsF));
					ASSERT(auto_coordinators.present());
					auto_coordinators_str = auto_coordinators.get().toString();
				}
				tr->set(fdb_cli::coordinatorsProcessSpecialKey, auto_coordinators_str);
			} else if (tokens.size() > 1) {
				state std::set<NetworkAddress> new_coordinators_addresses;
				state std::set<Hostname> new_coordinators_hostnames;
				state std::vector<std::string> newCoordinatorslist;
				state std::vector<StringRef>::iterator t;
				for (t = tokens.begin() + 1; t != tokens.end(); ++t) {
					try {
						if (Hostname::isHostname(t->toString())) {
							// We do not resolve hostnames here. We commit them as is.
							const auto& hostname = Hostname::parse(t->toString());
							if (new_coordinators_hostnames.count(hostname)) {
								fprintf(stderr,
								        "ERROR: passed redundant coordinators: `%s'\n",
								        hostname.toString().c_str());
								return true;
							}
							new_coordinators_hostnames.insert(hostname);
							newCoordinatorslist.push_back(hostname.toString());
						} else {
							const auto& addr = NetworkAddress::parse(t->toString());
							if (new_coordinators_addresses.count(addr)) {
								fprintf(
								    stderr, "ERROR: passed redundant coordinators: `%s'\n", addr.toString().c_str());
								return true;
							}
							new_coordinators_addresses.insert(addr);
							newCoordinatorslist.push_back(addr.toString());
						}
					} catch (Error& e) {
						if (e.code() == error_code_connection_string_invalid) {
							fprintf(
							    stderr, "ERROR: '%s' is not a valid network endpoint address\n", t->toString().c_str());
							return true;
						}
						throw;
					}
				}
				std::string new_coordinators_str = boost::algorithm::join(newCoordinatorslist, ",");
				tr->set(fdb_cli::coordinatorsProcessSpecialKey, new_coordinators_str);
			}
			wait(safeThreadFutureToFuture(tr->commit()));
			// commit should always fail here
			// If the commit succeeds, the coordinators change and the commit will fail with commit_unknown_result().
			ASSERT(false);
		} catch (Error& e) {
			state Error err(e);
			if (e.code() == error_code_special_keys_api_failure) {
				std::string errorMsgStr = wait(fdb_cli::getSpecialKeysFailureErrorMessage(tr));
				if (errorMsgStr == ManagementAPI::generateErrorMessage(CoordinatorsResult::NOT_ENOUGH_MACHINES) &&
				    notEnoughMachineResults < 1) {
					// we could get not_enough_machines if we happen to see the database while the cluster controller is
					// updating the worker list, so make sure it happens twice before returning a failure
					notEnoughMachineResults++;
					wait(delay(1.0));
					tr->reset();
					continue;
				} else if (errorMsgStr ==
				           ManagementAPI::generateErrorMessage(CoordinatorsResult::SAME_NETWORK_ADDRESSES)) {
					if (retries)
						printf("Coordination state changed\n");
					else
						printf("No change (existing configuration satisfies request)\n");
					return true;
				} else {
					fprintf(stderr, "ERROR: %s\n", errorMsgStr.c_str());
					return false;
				}
			}
			wait(safeThreadFutureToFuture(tr->onError(err)));
			++retries;
		}
	}
}

} // namespace

namespace fdb_cli {

const KeyRef clusterDescriptionSpecialKey = "\xff\xff/configuration/coordinators/cluster_description"_sr;
const KeyRef configDBSpecialKey = "\xff\xff/configuration/coordinators/config_db"_sr;
const KeyRef coordinatorsAutoSpecialKey = "\xff\xff/management/auto_coordinators"_sr;
const KeyRef coordinatorsProcessSpecialKey = "\xff\xff/configuration/coordinators/processes"_sr;

ACTOR Future<bool> coordinatorsCommandActor(Reference<IDatabase> db, std::vector<StringRef> tokens) {
	if (tokens.size() < 2) {
		wait(printCoordinatorsInfo(db));
		return true;
	} else {
		bool result = wait(changeCoordinators(db, tokens));
		return result;
	}
}

CommandFactory coordinatorsFactory(
    "coordinators",
    CommandHelp(
        "coordinators auto|<ADDRESS>+ [description=new_cluster_description]",
        "change cluster coordinators or description",
        "If 'auto' is specified, coordinator addresses will be choosen automatically to support the configured "
        "redundancy level. (If the current set of coordinators are healthy and already support the redundancy level, "
        "nothing will be changed.)\n\nOtherwise, sets the coordinators to the list of IP:port pairs specified by "
        "<ADDRESS>+. An fdbserver process must be running on each of the specified addresses.\n\ne.g. coordinators "
        "10.0.0.1:4000 10.0.0.2:4000 10.0.0.3:4000\n\nIf 'description=desc' is specified then the description field in "
        "the cluster\nfile is changed to desc, which must match [A-Za-z0-9_]+."));
} // namespace fdb_cli
