#include <algorithm>
#include <unordered_set>

#include <libfilezilla/util.hpp>

#include "file_based_authenticator.hpp"
#include "error.hpp"

#include "../serialization/archives/xml.hpp"
#include "../impersonator/client.hpp"
#include "../remove_event.hpp"
#include "../logger/type.hpp"

#ifdef HAVE_CONFIG_H
#   include "config_modules.hpp"
#endif

namespace fz::authentication {

namespace {
	using xml_archiver = util::xml_archiver<authentication::file_based_authenticator::groups, authentication::file_based_authenticator::users>;
}

class file_based_authenticator::worker
{
public:
	class operation;

	worker(file_based_authenticator &owner, std::string_view name, event_handler *target, logger::modularized::meta_map meta_for_logging)
		: owner_(owner)
		, async_handler_(target->event_loop_)
		, name_(name)
		, target_(target)
		, logger_(owner.logger_, {}, std::move(meta_for_logging))
	{
	}


private:
	friend file_based_authenticator;

	void start(std::string_view realm, address_type family, std::string_view ip, const methods_list &methods);
	void next(const methods_list &methods, available_methods &&available_methods);
	void yield(error e, shared_user &&su = {}, available_methods &&am = {});

	void remove()
	{
		if (target_) {
			workers to_remove;

			{
				scoped_lock	lock(owner_.workers_mutex_);
				to_remove.splice(to_remove.begin(), owner_.workers_, self_in_workers_);
				target_ = nullptr;
			}
		}
	}

	file_based_authenticator &owner_;
	async_handler async_handler_;

	std::string id_{};
	std::string name_{};
	user_entry u_{};
	impersonation_context impersonation_context_{};
	event_handler *target_{};
	logger::modularized logger_;

	bool bypass_default_impersonator_{};

	workers::iterator self_in_workers_;

	bounded_async_task bounded_async_task_;
};

class file_based_authenticator::worker::operation: public authenticator::operation
{
public:
	shared_user get_user() override;
	available_methods get_methods() override;
	error get_error() override;

	bool next(const methods_list &methods) override;
	void stop() override;

	operation(worker &w, shared_user &&su, available_methods &&m, error e);

private:
	friend worker;

	worker *worker_;
	shared_user shared_user_;
	available_methods methods_;
	error error_;
};

shared_user file_based_authenticator::worker::operation::get_user()
{
	if (!error_ &&  !methods_.is_auth_necessary())
		return shared_user_;

	return {};
}

available_methods file_based_authenticator::worker::operation::get_methods()
{
	return methods_;
}

error file_based_authenticator::worker::operation::get_error()
{
	return error_;
}

bool file_based_authenticator::worker::operation::next(const methods_list &methods)
{
	if (worker_) {
		if (error_.is_permament()) {
			worker_->yield(error_, {}, std::move(methods_));
		}
		else {
			worker_->bounded_async_task_ = worker_->owner_.bounded_thread_pool_.spawn([w = worker_, methods, methods_ = std::move(methods_)]() mutable {
				w->next(methods, std::move(methods_));
			}, [w = worker_]() mutable {
				w->logger_.log(logmsg::error, L"Timed out while waiting for a worker thread to free up.");
				w->yield(error::timeout);
			});

			if (!worker_->bounded_async_task_) {
				worker_->logger_.log(logmsg::error, L"Couldn't spawn a worker thread. This is an internal error.");
				worker_->yield(error::internal);
			}
		}

		worker_ = nullptr;

		return true;
	}

	return false;
}

void file_based_authenticator::worker::operation::stop()
{
	if (worker_) {
		worker_->logger_.log_u(logmsg::debug_debug, L"operation %p stop() erasing worker %p", this, worker_);

		worker_->remove();
		worker_ = nullptr;
	}
}

file_based_authenticator::worker::operation::operation(worker &w, shared_user &&su, available_methods &&m, error e)
	: worker_(&w)
	, shared_user_(std::move(su))
	, methods_(std::move(m))
	, error_(std::move(e))
{
	if (w.logger_.should_log(logmsg::debug_debug))
		w.logger_.log_u(logmsg::debug_debug, L"Worker %p created new operation %p, with shared_user = %p, methods = [%s], error = %s", &w, this, shared_user_.get(), methods_, error_);
}

file_based_authenticator::file_based_authenticator(thread_pool &thread_pool, event_loop &event_loop, logger_interface &logger, rate_limit_manager &rlm, options opts)
	: thread_pool_(thread_pool)
	, event_loop_(event_loop)
	, logger_(logger, "File-based Authenticator")
	, rlm_(rlm)
	, opts_(std::move(opts))
	, bounded_thread_pool_(thread_pool_, opts_.max_concurrent_auths(), opts_.auth_wait_timeout())
	, async_handler_(event_loop)
{
	for (auto &idp: opts_.idps()) {
		auto it = std::find_if(id_providers_.begin(), id_providers_.end(), [&](auto &data) {
			return data.idp->get_name() == idp.get().get_name() || data.idp == &idp.get();
		});

		if (it != id_providers_.end()) {
			continue;
		}

		id_providers_.push_back({&idp.get(), {}});
	}
}

file_based_authenticator::file_based_authenticator(thread_pool &thread_pool, event_loop &event_loop, logger_interface &logger, rate_limit_manager &rlm, native_string groups_path, native_string users_path, options opts)
	: file_based_authenticator(thread_pool, event_loop, logger, rlm, std::move(opts))
{
	auto a = std::make_unique<xml_archiver>(rlm.event_loop_);
	a->set_values(
		{groups_, { "", groups_path }},
		{users_, { "", users_path }}
	);

	xml_archiver_ = std::move(a);

	load();
}


file_based_authenticator::file_based_authenticator(thread_pool &thread_pool, event_loop &event_loop, logger_interface &logger, rate_limit_manager &rlm, native_string groups_path, fz::authentication::file_based_authenticator::groups &&groups, native_string users_path, fz::authentication::file_based_authenticator::users &&users, options opts)
	: file_based_authenticator(thread_pool, event_loop, logger, rlm, std::move(opts))
{
	auto a = std::make_unique<xml_archiver>(rlm.event_loop_, fz::duration::from_milliseconds(100), &mutex_);

	a->set_values(
		{groups_, { "", groups_path }},
		{users_, { "", users_path }}
	);

	xml_archiver_ = std::move(a);

	set_groups_and_users(std::move(groups), std::move(users));
}

file_based_authenticator::~file_based_authenticator()
{
}

bool file_based_authenticator::load()
{
	groups groups;
	users users;

	if (load_into(groups, users) == 0) {
		set_groups_and_users(std::move(groups), std::move(users));
		return true;
	}

	return false;
}

bool file_based_authenticator::save(util::xml_archiver_base::event_dispatch_mode mode)
{
	if (xml_archiver_) {
		return xml_archiver_->save_now(mode) == 0;
	}

	return false;
}

void file_based_authenticator::set_save_result_event_handler(event_handler *handler)
{
	if (xml_archiver_) {
		xml_archiver_->set_event_handler(handler);
	}
}

void file_based_authenticator::save_later()
{
	if (xml_archiver_) {
		xml_archiver_->save_later();
	}
}

void file_based_authenticator::sanitize()
{
	// Sanitize groups
	for (auto it = groups_.begin(); it != groups_.end();) {
		auto &g = *it;

		//Disallow invalid chars in group names
		if (g.first.empty() || g.first.find_first_of(groups_.invalid_chars_in_name) != std::string::npos) {
			logger_.log_u(logmsg::error, L"Group has invalid name \"%s\", removing it from the list", g.first);

			it = groups_.erase(it);
			continue;
		}

		it = ++it;
	}

	for (auto &idp_data: id_providers_) {
		create_idp_user_if_not_existing(idp_data);
	}

	// Sanitize users
	for (auto it = users_.begin(); it != users_.end();) {
		auto &u = *it;

		//Disallow invalid chars in usernames
		if (u.first.empty() || (u.first.find_first_of(users_.invalid_chars_in_name) != std::string::npos && !is_idp_user(u))) {
			logger_.log_u(logmsg::error, L"User has invalid name \"%s\", removing it from the list", u.first);

			it = users_.erase(it);
			continue;
		}

		if (is_idp_user(u)) {
			auto idpd = get_idp_data_by_name(u.second.identity->idp);
			if (!idpd) {
				logger_.log_u(logmsg::warning, L"IDP user %s exists but its identity provider \"%s\" is not recognized. Login with this IDP will not be possible.", u.first, u.second.identity->idp);
				++it;
				continue;
			}

			if (!u.second.identity->uid.empty()) {
				logger_.log_u(logmsg::warning, L"IDP user %s had a UID set to %s. This shouldn't happend. The UID has been reset.", u.first, u.second.identity->uid);
				u.second.identity->uid.clear();
			}
		}

		// remove references to non-existing or duplicated groups
		u.second.groups.erase(std::remove_if(u.second.groups.begin(), u.second.groups.end(), [&](auto &g) {
			auto already_seen = [set = std::unordered_set<std::string>{}](const std::string &s) mutable {
				auto [it, inserted] = set.insert(s);
				return !inserted;
			};

			bool group_doesnt_exist = groups_.count(g) == 0;
			if (group_doesnt_exist)
				logger_.log_u(logmsg::warning, L"Group [%s] referenced by user [%s] does not exist or has been in a previous sanitizing step. Ignoring.", g, u.first);

			bool duplicated = already_seen(g);
			if (duplicated)
				logger_.log_u(logmsg::warning, L"Group [%s] is referenced multiple times by user [%s]. Ignoring the excess references", g, u.first);

			return group_doesnt_exist || duplicated;
		}), u.second.groups.end());

		if (!u.second.methods.is_auth_possible()) {
			logger_.log_u(logmsg::debug_info, L"User \"%s\" does not have any auth methods configured. Login will not be possible. [%s].", u.first);
		}
		else
		if (!u.second.credentials.is_valid_for(u.second.methods, logger_)) {
			logger_.log_u(logmsg::warning, L"User \"%s\" has auth methods [%s] that do not match the credentials. Login will not be possible.", u.first, u.second.methods);
		}

#ifdef ENABLE_FZ_WEBUI
		// Enable token credentials for all users
		// FIXME: this ought to be decoupled from the authenticator itself
		static const auto token_set = [] {
			auto set = fz::authentication::methods_set();
			set.add<fz::authentication::method::token>();
			return set;
		}();

		if (!u.second.methods.has(token_set)) {
			u.second.methods.push_back(token_set);
		}
#endif

		it = ++it;
	}

	if (users_.default_impersonator.index() != users::impersonator::any(users::impersonator::native()).index()) {
		logger_.log_u(logmsg::debug_warning, L"The type of the default impersonator is not valid on this platform. Resetting the default impersonator.");

		users_.default_impersonator = users::impersonator::native();
	}
}

void file_based_authenticator::update()
{
	sanitize();

	for (auto l_it = group_limiters_.begin(); l_it != group_limiters_.end();) {
		if (auto g_it = groups_.find(l_it->first); g_it == groups_.end()) {
			l_it = group_limiters_.erase(l_it);
		}
		else {
			update_group_limiters(l_it->second, *g_it);
			++l_it;
		}
	}

	auto default_impersonator_token = users_.default_impersonator.get_token();

	for (auto wu_it = weak_users_map_.begin(); wu_it != weak_users_map_.end();) {
		if (auto su = wu_it->second.lock(); !su) {
			wu_it = weak_users_map_.erase(wu_it);
		}
		else {
			std::shared_ptr<tvfs::mount_tree> mt;
			std::shared_ptr<tvfs::backend> b;

			{
				auto locked_su = su->lock();
				auto u_it = users_.find(locked_su->id);

				auto has_filesystem_impersonator = [&] {
					auto const &i = locked_su->impersonation_token;
					return i && locked_su->name == fz::to_utf8(i.username());
				}();

				// If the user has been deleted
				// Or if it's been disabled
				// Or if it doesn't have a filesystem impersonator and the default impersonator token has changed
				if (u_it == users_.end() || !u_it->second.enabled || !(has_filesystem_impersonator || (locked_su->impersonation_token == default_impersonator_token))) {
					// Then signal that the shared user must be disposed of, to whomever else might be holding its pointer.
					// This will also make sessions log out if it's this user that was holding them open.
					locked_su->id.clear();
					wu_it = weak_users_map_.erase(wu_it);
				}
				else {
					update_shared_user(*locked_su, u_it->second, logger_);

					mt = locked_su->mount_tree;
					b = locked_su->tvfs_backend;

					++wu_it;
				}
			}


			if (b) {
				b->set_credentials(users_.mount_credentials, async_receive(async_handler_) >> [this, mt = std::move(mt), su = std::move(su), b](auto) mutable {
					tvfs::async_autocreate_directories(std::move(mt), std::move(b), async_receive(async_handler_) >> [su = std::move(su)]() mutable {
						notify(su);
					});
				});

			}
			else {
				notify(su);
			}
		}
	}

	// Update the user maps for the users managed by the IDPs.
	for (auto &idpd: id_providers_) {
		idpd.managed_users.clear();
	}

	for (auto u_it = users_.begin(); u_it != users_.end(); ++u_it) {
		if (auto i = u_it->second.identity; i && u_it->second.enabled && !i->uid.empty()) {
			if (auto idpd = get_idp_data_by_name(i->idp)) {
				logger_.log(logmsg::debug_info, L"Local user '%s' is managed by IDP '%s'", u_it->first, i->idp);
				auto [it, emplaced] = idpd->managed_users.emplace(i->uid, u_it);
				if (!emplaced) {
					logger_.log(logmsg::warning, L"Local user '%s' has the same UID as local user '%s', both managed by IDP '%s'. This should not happen. User '%s' will thus be disabled.", u_it->first, it->second->first, i->idp, u_it->first);
					u_it->second.enabled = false;
				}
			}
		}
	}
}

void file_based_authenticator::create_idp_user_if_not_existing(const idp_data &idpd) {
	auto res = users_.try_emplace(username_for_idp(*idpd.idp), user_entry{});
	if (!res.second) {
		return;
	}

	auto &u = res.first->second;

	apply_user_properties(idpd.idp->get_default_user_properties(), u, logger_);

	u.enabled = false;
	u.identity.emplace().idp = idpd.idp->get_name();
}

error file_based_authenticator::apply_user_properties(identity_provider::user_properties &&props, user_entry &uentry, logger_interface &logger)
{
	if (props.methods) {
		uentry.methods = std::move(*props.methods);
	}

	if (props.description) {
		uentry.description = std::move(*props.description);
	}

	if (props.password) {
		if (!uentry.methods.can_verify(method::password())) {
			logger.log(logmsg::error, L"User is required to have password authentication, but the password method is not enabled. This is an internal error.");
			return error::internal;
		}

		uentry.credentials.password = std::move(*props.password);
		if (!uentry.credentials.password) {
			uentry.methods.remove(method::password());
		}
	}

	if (props.mount_table) {
		uentry.mount_table.insert(uentry.mount_table.begin(), std::make_move_iterator(props.mount_table->begin()), std::make_move_iterator(props.mount_table->end()));
	}

	if (props.groups) {
		uentry.groups = std::move(*props.groups);
	}

	return error::none;
}

error file_based_authenticator::get_auth_data_for_user(const std::string &name, std::string &uid, user_entry &uentry, impersonation_context &impersonation, bool &bypass_default_impersonator)
{
	identity_provider::user_info uinfo;

	auto get_auth_data_for_identity_profile = [&](scoped_lock &lock, const users::iterator &it, idp_data *idpd = nullptr) -> authentication::error {
		if (!it->second.enabled) {
			return error::user_nonexisting;
		}

		if (!it->second.identity) {
			logger_.log_raw(logmsg::error, L"No identity provider set. This is an internal error.");
			return error::internal;
		}

		const auto idp_name = it->second.identity->idp;
		decltype(idpd->managed_users) *managed_users{};

		if (!idpd) {
			// In case this a local user that maps one from an IDP, we require the UID to be set.
			if (it->second.identity->uid.empty()) {
				logger_.log_u(logmsg::error, L"UID for user '%s' with identity provider '%s' is not set, authentication cannot continue. This is an internal error.", name, it->second.identity->idp);
				return error::internal;
			}

			idpd = get_idp_data_by_name(idp_name);
			if (!idpd) {
				logger_.log_u(logmsg::error, L"Identity provider '%s' not registered, authentication cannot continue. This is an internal error.", it->second.identity->idp);
				return error::internal;
			}
		}
		else {
			managed_users = &idpd->managed_users;

			if (idp_name != idpd->idp->get_name()) {
				logger_.log_u(logmsg::error, L"Mismatch between id provider names ('%s' vs '%s'), authentication cannot continue. This is an internal error.", idpd->idp->get_name(), idp_name);
				return error::internal;
			}
		}

		logger_.log(logmsg::debug_debug, L"Getting auth data for user '%s' via IDP '%s'", name, idp_name);

		uid = it->first;
		uentry = it->second;

		lock.unlock();

		auto error = idpd->idp->get_user_info(name, uentry.identity->parameters, uinfo);

		lock.lock();

		if (!error) {
			if (!uentry.identity->uid.empty() && uinfo.uid != uentry.identity->uid) {
				logger_.log_u(logmsg::error, L"UID for user '%s' with identity provider '%s' has changed: old = [%s], new = [%s]. For security reasons authentication cannot continue. Please review the user.", name, idp_name, uentry.identity->uid, uinfo.uid);
				error = error::internal;
			}

			if (managed_users) {
				if (auto it = managed_users->find(uinfo.uid); it != managed_users->end()) {
					logger_.log_u(logmsg::status, L"User '%s' on IDP '%s' is found to be the same as local user '%s', because their UIDs match.", name, idp_name, it->second->first);
					uid = it->second->first;
					uentry = it->second->second;
				}
			}

			impersonation.uid = uinfo.uid;
			impersonation.username = uinfo.account;
		}

		return error;
	};

	auto get_auth_data_for_file_based_user = [&](scoped_lock &lock, const users::iterator &it) -> authentication::error {
		if (name.find_first_of("<>") != name.npos) {
			return error::user_nonexisting;
		}

		if (!it->second.enabled) {
			return error::user_disabled;
		}

		if (it->second.identity) {
			auto error = get_auth_data_for_identity_profile(lock, it);
			if (error == error::user_nonexisting) {
				logger_.log_u(logmsg::error, L"User '%s' is configured to use IDP '%s', but that user doesn't exist on the IDP. This is an internal error.", name, it->second.identity->idp);
				error = error::internal;
			}

			return error;
		}

		uid = it->first;
		uentry = it->second;

		return error::none;
	};

	auto get_data = [&]() -> error {
		scoped_lock lock(mutex_);

		if (auto it = users_.find(name); it != users_.end()) {
			auto error = get_auth_data_for_file_based_user(lock, it);

			if (error != error::user_nonexisting) {
				return error;
			}
		}

		if (auto it = temp_users_.find(name); it != temp_users_.end()) {
			auto error = get_auth_data_for_file_based_user(lock, it);

			if (error != error::user_nonexisting) {
				bypass_default_impersonator = true;
				return error;
			}
		}

		for (auto &idpd: id_providers_) {
			if (auto it = users_.find(username_for_idp(*idpd.idp)); it != users_.end()) {
				auto error = get_auth_data_for_identity_profile(lock, it, &idpd);
				if (error != error::user_nonexisting) {
					return error;
				}
			}
		}

		return error::user_nonexisting;
	};

	auto error = get_data();

	if (!error) {
		apply_user_properties(std::move(uinfo.properties), uentry, logger_);
	}

	return error;
}

error file_based_authenticator::check_realm(const std::string_view realm, const std::string &name, const user_entry &u)
{
	scoped_lock lock(mutex_);

	static constexpr std::string_view known_realms[] = {
		"ftp",
		"ftps",
		"webui"
	};

	static constexpr enum realm::status default_status = realm::enabled;

	if (std::find(std::begin(known_realms), std::end(known_realms), realm) == std::end(known_realms)) {
		logger_.log(logmsg::error, L"Realm %s not known while authenticating user %s. This should not happen, contact the administrator.", realm, name);
		return error::internal;
	}

	auto get_status = [&](const std::vector<struct realm> &realms) {
		auto it = std::find_if(std::begin(realms), std::end(realms), [&](const struct realm &r) {
			return r.name == realm;
		});

		if (it != std::end(realms)) {
			return it->status;
		}

		return realm::deferred;
	};

	enum realm::status status = get_status(u.realms);
	if (status == realm::deferred) {
		// Look in the groups
		for (auto &ug: u.groups) {
			auto git = groups_.find(ug);
			if (git != groups_.end()) {
				status = get_status(git->second.realms);
				if (status != realm::deferred) {
					break;
				}
			}
		}
	}

	if (status == realm::deferred) {
		status = get_status(users_.realms);
	}

	if (status == realm::deferred) {
		status = default_status;
	}

	if (status == realm::enabled) {
		logger_.log(logmsg::debug_info, L"Realm %s is enabled for user %s. Continuing authentication.", realm, name);
		return error::none;
	}

	if (status == realm::disabled) {
		logger_.log(logmsg::error, L"Realm %s is disabled for user %s. Authentication cannot continue.", realm, name);
		return error::realm_disabled;
	}

	logger_.log(logmsg::error, L"Unexpected state for check_realm() while authenticating user %s. This should not happen, contact the administrator.", name);
	return error::internal;
}

void file_based_authenticator::set_groups_and_users(file_based_authenticator::groups &&groups, file_based_authenticator::users &&users)
{
	scoped_lock lock(mutex_);

	groups_ = std::move(groups);
	users_ = std::move(users);

	update();
}

void file_based_authenticator::get_groups_and_users(file_based_authenticator::groups &groups, file_based_authenticator::users &users)
{
	scoped_lock lock(mutex_);

	groups = groups_;
	users = users_;
}

identity_provider *file_based_authenticator::get_identity_provider(std::string_view name) const
{
	scoped_lock lock(mutex_);

	for (auto &d: id_providers_) {
		if (d.idp->get_name() == name) {
			return d.idp;
		}
	}

	return nullptr;
}

std::vector<std::reference_wrapper<identity_provider>> file_based_authenticator::get_identity_providers() const
{
	std::vector<std::reference_wrapper<identity_provider>> idps;
	idps.reserve(id_providers_.size());

	scoped_lock lock(mutex_);

	for (auto &i: id_providers_) {
		if (i.idp) {
			idps.push_back(*i.idp);
		}
	}

	return idps;
}

std::vector<std::string> file_based_authenticator::get_identity_provider_names() const
{
	std::vector<std::string> idps;

	idps.reserve(id_providers_.size());

	scoped_lock lock(mutex_);

	for (auto &i: id_providers_) {
		if (i.idp) {
			idps.push_back(i.idp->get_name());
		}
	}

	return idps;
}

std::pair<std::string, std::string> file_based_authenticator::make_temp_user(tvfs::mount_table mt)
{
	logger_.log_raw(logmsg::status, L"Creating a temporary user...");

	std::string name;
	std::string password = hex_encode<std::string>(random_bytes(16));

	user_entry ue;
	ue.mount_table = std::move(mt);
	ue.credentials.password = any_password(default_password(password));
	ue.methods = { authentication::method::password() };

	scoped_lock lock(mutex_);

	// The likelihood of a 128 bits random username to be already in the user maps is insignificant.
	// Nonetheless, let's adhere to the best practices of defensive programming and check for collisions.
	// It looks and feels silly, but... you never know.
	constexpr unsigned max_tries = 5;

	for (unsigned tries = 0; tries < max_tries; ++tries) {
		name = hex_encode<std::string>(random_bytes(16));

		if (users_.count(name) == 0 && temp_users_.try_emplace(name, std::move(ue)).second) {
			logger_.log_u(logmsg::status, L"Successfully created temporary user '%s'.", name);
			return {std::move(name), std::move(password)};
		}
	}

	logger_.log_raw(logmsg::error, L"Couldn't create the temporary user.");
	return {};
}

bool file_based_authenticator::remove_temp_user(const std::string &name)
{
	scoped_lock lock(mutex_);

	if (temp_users_.erase(name)) {
		logger_.log_u(logmsg::status, L"Succefully removed temporary user '%s'.", name);
		return true;
	}

	logger_.log_u(logmsg::status, L"Couldn't remove temporary user '%s'.", name);
	return false;
}

void file_based_authenticator::authenticate(std::string_view realm, std::string_view name, const methods_list &methods, address_type family, std::string_view ip, event_handler &target, logger::modularized::meta_map meta_for_logging)
{
	auto worker = [&]() {
		scoped_lock lock(workers_mutex_);
		auto &worker = workers_.emplace_front(std::make_shared<class worker>(*this, name, &target, std::move(meta_for_logging)));
		worker->self_in_workers_ = workers_.begin();
		return worker;
	}();

	worker->bounded_async_task_ = bounded_thread_pool_.spawn([w = worker, realm = std::move(realm), family, ip = std::move(ip), methods = std::move(methods)]() mutable {
		w->start(realm, family, ip, methods);
	}, [w = worker]() mutable {
		w->logger_.log(logmsg::error, L"Timed out while waiting for a worker thread to free up.");
		w->yield(error::timeout);
	});

	if (!worker->bounded_async_task_) {
		worker->logger_.log(logmsg::error, L"Couldn't spawn a worker thread. This is an internal error.");
		worker->yield(error::internal);
	}
}

void file_based_authenticator::stop_ongoing_authentications(event_handler &target)
{
	{
		workers to_remove;

		{
			fz::scoped_lock lock(workers_mutex_);

			for (auto it = workers_.begin(), end = workers_.end(); it != end;) {
				auto next = std::next(it);
				auto &w = **it;

				if (w.target_ == &target) {
					to_remove.splice(to_remove.begin(), workers_, it);
					w.target_ = nullptr;
				}

				it = next;
			}
		}
	}

	remove_events<operation::result_event>(&target, *this);
}

file_based_authenticator::idp_data *file_based_authenticator::get_idp_data_by_name(std::string_view name)
{
	auto idp_it = std::find_if(id_providers_.begin(), id_providers_.end(), [&](const auto &data) {
		return data.idp->get_name() == name;
	});

	if (idp_it == id_providers_.end()) {
		return nullptr;
	}

	return std::addressof(*idp_it);
}

identity_provider *file_based_authenticator::get_id_provider_by_name(std::string_view name)
{
	auto idpd = get_idp_data_by_name(name);

	return idpd ? idpd->idp : nullptr;
}

void file_based_authenticator::update_shared_user(user &user, const user_entry &entry, logger::modularized &logger_)
{
	logger::modularized logger(logger_, {}, { { "user", user.name }});

	user.mount_tree = std::make_shared<tvfs::mount_tree>(tvfs::mount_table{}, tvfs::placeholders::map {
		{ tvfs::placeholders::user_name, fz::to_native_from_utf8(user.name) },
		{ tvfs::placeholders::home_dir, user.home },
		{ tvfs::placeholders::anything(fzT("%p")), fz::tvfs::placeholders::make_invalid_value(fzT("%%<%p> is not a recognized placeholder"))}
	}, logger);

	if (!user.limiter)
		user.limiter = std::make_shared<rate_limiter>(&rlm_);

	user.limiter->set_limits(entry.rate_limits.inbound, entry.rate_limits.outbound);

	user.session_inbound_limit = entry.rate_limits.session_inbound;
	user.session_outbound_limit = entry.rate_limits.session_outbound;

	user.session_open_limits.files = entry.session_open_limits.files;
	user.session_open_limits.directories = entry.session_open_limits.directories;

	user.session_count_limiter.set_limit(entry.session_count_limit);

	user.extra_limiters.clear();
	user.extra_limiters.reserve(entry.groups.size());

	user.extra_session_count_limiters.clear();
	user.extra_session_count_limiters.reserve(entry.groups.size());

	static auto update_limit = [](const auto &g, auto &u, auto unlimited) {
		if (g != unlimited) {
			if (u == unlimited || g < u)
				u = g;
		}
	};

	for (auto git = entry.groups.crbegin(), gend = entry.groups.crend(); git != gend; ++git) {
		auto g = groups_.find(*git);
		if (g != groups_.end()) {
			logger.insert_meta("group", g->first);

			user.mount_tree->merge_with(g->second.mount_table, logger);
			auto &gl = get_or_make_group_limiters(*g);
			user.extra_limiters.push_back(gl.shared_rate_limiter);
			user.extra_session_count_limiters.push_back(gl.session_count_limiter);

			update_limit(g->second.rate_limits.session_inbound, user.session_inbound_limit, rate::unlimited);
			update_limit(g->second.rate_limits.session_outbound, user.session_outbound_limit, rate::unlimited);

			update_limit(g->second.session_open_limits.files, user.session_open_limits.files, tvfs::open_limits::unlimited);
			update_limit(g->second.session_open_limits.directories, user.session_open_limits.directories, tvfs::open_limits::unlimited);
		}
	}

	logger.erase_meta("group");

	user.mount_tree->merge_with(entry.mount_table, logger);

	if (logger.should_log(logmsg::debug_info)) {
		logger.log_raw(logmsg::debug_info, L"Effective mount points:");
		user.mount_tree->dump("  > ", logger, logmsg::debug_info);
	}

	std::sort(user.extra_limiters.begin(), user.extra_limiters.end());
}

void file_based_authenticator::update_group_limiters(group_limiters &limiters, const file_based_authenticator::groups::value_type &g)
{
	limiters.shared_rate_limiter->set_limits(g.second.rate_limits.inbound, g.second.rate_limits.outbound);
	limiters.session_count_limiter->set_limit(g.second.session_count_limit);
}

std::shared_ptr<tvfs::backend> file_based_authenticator::make_shared_tvfs_backend(impersonation_token &token, logger::modularized &logger_)
{
	auto shared_tvfs_backend = [&]() -> std::shared_ptr<tvfs::backend> {
		if (token) {
			return std::make_shared<impersonator::client>(thread_pool_, this->logger_, token, opts_.impersonator_exe());
		}
		else {
			return std::make_shared<tvfs::backends::local_filesys>(this->logger_);
		}
	}();

	logger_.log(logmsg::debug_info, L"make_shared_tvfs_backend(\"%s\"): backend CREATED.", token.uid());

	if (!users_.mount_credentials.empty()) {
		shared_tvfs_backend->set_credentials(users_.mount_credentials, sync_receive >> [](auto){});
	}

	return shared_tvfs_backend;
}

shared_user file_based_authenticator::get_or_make_shared_user(const std::string &name, const user_entry &entry, const std::string &id, impersonation_context &&impersonation, logger::modularized &logger_)
{
	scoped_lock lock(mutex_);

	auto &weak_user_in_map = weak_users_map_[name];
	auto shared_user_in_map = weak_user_in_map.lock();

	if (shared_user_in_map) {
		// Has the token changed from what we already have?
		if (auto u = shared_user_in_map->lock(); !(u->impersonation_token == impersonation.token)) {
			// If so, we ought to let the old user's holders know that they're done with it.
			u->id.clear();
			notify(shared_user_in_map);

			shared_user_in_map.reset();
		}
	}

	if (!shared_user_in_map) {
		weak_user_in_map = shared_user_in_map = make_shared_user<util::locking_wrapper<authentication::user, fz::mutex&>>(mutex_, id, name);

		auto u = shared_user_in_map->lock();
		u->impersonation_token = std::move(impersonation.token);
		u->tvfs_backend = make_shared_tvfs_backend(u->impersonation_token, logger_);
		u->home = std::move(impersonation.home);

		update_shared_user(*u, entry, logger_);
	}

	return shared_user_in_map;
}

file_based_authenticator::group_limiters &file_based_authenticator::get_or_make_group_limiters(const groups::value_type &g)
{
	auto it = group_limiters_.find(g.first);

	if (it == group_limiters_.end()) {
		bool inserted;

		std::tie(it, inserted) = group_limiters_.insert({g.first, {
			std::make_shared<rate_limiter>(&rlm_),
			std::make_shared<util::limited_copies_counter>(fz::sprintf("group «%s»", g.first))
		}});

		if (inserted) {
			update_group_limiters(it->second, g);
		}
	}

	return it->second;
}


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

void file_based_authenticator::worker::start(std::string_view realm, address_type family, std::string_view ip, const methods_list &methods)
{
	{
		scoped_lock lock(owner_.workers_mutex_);
		if (!target_) {
			return;
		}
	}

	logger_.log_u(logmsg::debug_info, L"Invoking get_auth_data");

	auto error = owner_.get_auth_data_for_user(name_, id_, u_, impersonation_context_, bypass_default_impersonator_);
	if (error) {
		logger_.log_u(logmsg::debug_info, L"get_auth_data returned error: %s", error);
		return yield(error);
	}

	logger_.log_u(logmsg::debug_info, L"get_auth_data returned ok: %s", error);

	if (auto error = owner_.check_realm(realm, name_, u_)) {
		return yield(error);
	}

	{
		scoped_lock lock(owner_.mutex_);

		auto ip_in = [&](auto list) {
			if ((u_.*list).contains(ip, family)) {
				return true;
			}

			for (const auto &n: u_.groups) {
				if (const auto git = owner_.groups_.find(n); git != owner_.groups_.end()) {
					const auto &g = git->second;

					if ((g.*list).contains(ip, family)) {
						return true;
					}
				}
			}

			return false;
		};

		if (ip_in(&auth_entry::disallowed_ips) && !ip_in(&auth_entry::allowed_ips)) {
			return yield(error::ip_disallowed);
		}
	}

	if (!u_.credentials.is_valid_for(u_.methods, logger_)) {
		logger_.log_u(logmsg::error, L"User \"%s\" has auth methods [%s] that do not match the credentials. Login is not possible. This is an internal error, inform the administrator.", name_, u_.methods);
		return yield(error::internal);
	}

	next(methods, {});
}

void file_based_authenticator::worker::next(const methods_list &methods, available_methods &&available_methods)
{
	{
		scoped_lock lock(owner_.workers_mutex_);
		if (!target_) {
			return;
		}
	}

	error error{};

	if (logger_.should_log(logmsg::debug_debug))
		logger_.log_u(logmsg::debug_debug, "Invoked authenticate(%s) on worker %p, with available methods = [%s]", methods, this, available_methods);

	if (!error && !available_methods.is_auth_possible()) {
		available_methods = u_.methods;
	}

	if (!error && !methods.empty()) {
		if (logger_.should_log(logmsg::debug_verbose))
			logger_.log_u(logmsg::debug_verbose, "Authenticating user '%s'. Methods requested: %s. Available methods: [%s].", name_, methods, available_methods);

		if (!available_methods.can_verify(methods)) {
			error = error::auth_method_not_supported;
			logger_.log_u(logmsg::debug_verbose, "Authenticating user '%s' is not possible, no matching authentication methods are available.", name_);
		}

		if (!error && available_methods.is_auth_necessary()) {
			for (auto &method: methods) {
				if (!u_.credentials.verify(method, impersonation_context_, logger_)) {
					error = error::invalid_credentials;

					if (logger_.should_log(logmsg::debug_verbose)) {
						logger_.log_u(logmsg::debug_verbose, "Auth method %s NOT passed for user '%s'. Invalid credentials.", method, name_);
					}

					break;
				}

				if (logger_.should_log(logmsg::debug_verbose)) {
					logger_.log_u(logmsg::debug_verbose, "Auth method %s passed for user '%s'.", method, name_);
				}

				if (auto m = method.is<method::password>()) {
					if (auto pwd = u_.credentials.password.get(); pwd && !pwd->is<default_password>()) {
						logger_.log_u(logmsg::status, L"User '%s' has old style password, converting it into the new style one.", name_);
						*pwd = default_password(m->data);
						owner_.save_later();
					}
				}
			}

			// Only erase the methods from the list of available methods if all of the methods have been validated.
			if (!error) {
				if (!methods.just_verify()) {
					for (auto &method: methods) {
						available_methods.set_verified(method);
					}
				}
			}
		}
	}

	shared_user su;

	if (!error) {
		if ((methods.empty() && available_methods.is_auth_possible()) || available_methods.is_auth_necessary()) {
			if (logger_.should_log(logmsg::debug_debug))
				logger_.log_u(logmsg::debug_debug, "Authentication for user '%s' not complete. Remaning methods: [%s]", name_, available_methods);
		}
		else
		if (!methods.empty()) {
			if (logger_.should_log(logmsg::debug_verbose))
				logger_.log_u(logmsg::debug_verbose, "Authentication for user '%s' is complete.", name_);

			// Maybe use the default impersonator
			if (!impersonation_context_.token && !bypass_default_impersonator_) {
				scoped_lock lock(owner_.mutex_);

				if (auto imp = owner_.users_.default_impersonator.native(); imp && imp->enabled) {
					logger_.log_u(logmsg::debug_verbose, "User '%s' has no filesystem impersonator of its own but a default one for system user '%s' has been defined.", name_, imp->name);

					impersonation_context_.token = imp->get_token();

					if (!impersonation_context_.token) {
						error = error::internal;

						logger_.log_u(logmsg::error, "Couldn't get the impersonation token for the default system user '%s', refusing to log in. Double check your settings!", imp->name);
					}
				}
			}

			if (!error) {
				if (impersonation_context_.token && owner_.opts_.impersonator_exe().empty()) {
					error = error::internal;

					logger_.log_u(logmsg::error, L"Filesystem impersonation has been requested, but no impersonator executable has been set, refusing to log in. This is an internal error.");
				}
				else {
					su = owner_.get_or_make_shared_user(impersonation_context_.username.empty() ? name_ : fz::to_utf8(impersonation_context_.username), u_, id_, std::move(impersonation_context_), logger_);
				}
			}
		}
	}

	yield(error, std::move(su), std::move(available_methods));
}

void file_based_authenticator::worker::yield(error e, shared_user &&su, available_methods &&am)
{
	{
		scoped_lock lock(owner_.workers_mutex_);
		if (!target_) {
			return;
		}
	}

	if (su) {
		if (auto u = su->lock()) {
			logger_.log_u(logmsg::debug_verbose, L"yield impersonation_token: { username: \"%s\", home: \"%s\", uid: \"%s\" }", u->impersonation_token.username(), u->impersonation_token.home(), u->impersonation_token.uid());

			auto op = std::make_unique<operation>(*this, std::move(su), std::move(am), e);

			// Clients of the authenticator MUST invoke stop_ongoing_authentications() when the handler for the authentication is about to be killed.
			// When that happens, the handler for the next async op will be deleted too, which in turn will avoid dispatching the completion event.
			// Hence, it's safe to use target.event_loop_, because the async handler will live less than the target handler, which will live less than its loop.
			return tvfs::async_autocreate_directories(u->mount_tree, u->tvfs_backend, async_receive(async_handler_)
			>> [&, op = std::move(op)]() mutable {
				scoped_lock lock(owner_.workers_mutex_);
				if (target_) {
					target_->send_event<operation::result_event>(owner_, std::move(op));
				}
			});
		}
		else {
			logger_.log_u(logmsg::error, L"Authentication succeeded but the shared_user couldn't be locked. This is an internal error, inform the administrator.");
			su.reset();
			e = error::internal;
		}
	}

	scoped_lock lock(owner_.workers_mutex_);
	if (target_) {
		auto op = std::make_unique<operation>(*this, std::move(su), std::move(am), e);
		target_->send_event<operation::result_event>(owner_, std::move(op));
	}
}


}
