#include <cassert>

#include "throttled_authenticator.hpp"
#include "../remove_event.hpp"
#include "../hostaddress.hpp"

namespace fz::authentication {

class throttled_authenticator::worker: public event_handler, public enabled_for_receiving<worker> {
	class operation;

public:
	worker(throttled_authenticator &owner, std::string_view realm, std::string_view name, address_type family, std::string_view ip, event_handler *target, logger::modularized::meta_map meta_for_logging)
		: event_handler{owner.event_loop_}
		, realm_(realm)
		, name_(name)
		, family_(family)
		, ip_(ip)
		, target_(target)
		, owner_(owner)
		, meta_for_logging_(std::move(meta_for_logging))
		, logger_(owner.logger_, {}, meta_for_logging_)
	{
	}

	bool try_to_authenticate(const methods_list &methods, std::unique_ptr<authenticator::operation> next_op = {});
	bool authenticate();

	void record_failure();

	~worker() override {
		remove_handler_and_stop_receiving();

		owner_.wrapped_.stop_ongoing_authentications(*this);

		if (record_failure_about_failed_none_auth_on_destruction_) {
			logger_.log(logmsg::debug_info, L"No other auth method was attempted, thus record the 'none' authentication failure.");
			record_failure();
		}
		else
		if (authenticating_) {
			logger_.log(logmsg::debug_info, L"Destroying while auth still in progress: recording failure to mitigate a possible DoS.");
			record_failure();
		}
	}

	void remove()
	{
		workers_t to_remove;

		{
			fz::scoped_lock lock(owner_.mutex_);
			to_remove.splice(to_remove.begin(), owner_.workers_,  self_in_workers_);
		}
	}

private:
	friend throttled_authenticator;

	void operator()(const event_base &ev) override;
	void on_operation_result(authenticator &, std::unique_ptr<authenticator::operation> &op);
	void on_throttler_permitted_event();

	std::string realm_{};
	std::string name_{};
	methods_list methods_{};
	address_type family_{};
	std::string ip_{};
	event_handler *target_{};
	throttled_authenticator &owner_;
	logger::modularized::meta_map meta_for_logging_;
	logger::modularized logger_;

	std::size_t number_of_failed_none_auths_to_report_{};
	bool record_failure_about_failed_none_auth_on_destruction_{};
	bool authenticating_{};
	std::unique_ptr<authenticator::operation> next_op_{};

	workers_t::iterator self_in_workers_;
};

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

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

	operation(worker &w, std::unique_ptr<authenticator::operation> &&wrapped);

private:
	worker *worker_;
	std::unique_ptr<authenticator::operation> wrapped_;
};

shared_user throttled_authenticator::worker::operation::get_user()
{
	return wrapped_->get_user();
}

available_methods throttled_authenticator::worker::operation::get_methods()
{
	return wrapped_->get_methods();
}

error throttled_authenticator::worker::operation::get_error()
{
	return wrapped_->get_error();
}

bool throttled_authenticator::worker::operation::next(const methods_list &methods)
{
	bool res = false;

	if (worker_) {
		fz::scoped_lock lock(worker_->owner_.mutex_);
		res = worker_->try_to_authenticate(methods, std::move(wrapped_));

		worker_ = nullptr;
	}

	return res;
}

void throttled_authenticator::worker::operation::stop()
{
	authentication::stop(std::move(wrapped_));

	if (worker_) {
		fz::scoped_lock lock(worker_->owner_.mutex_);

		worker_->authenticating_ = false;
		worker_->logger_.log_u(logmsg::debug_debug, L"operation %p stop() erasing worker %p", this, worker_);
		worker_->remove();
		worker_ = nullptr;
	}
}

throttled_authenticator::worker::operation::operation(worker &w, std::unique_ptr<authenticator::operation> &&wrapped)
	: worker_(&w)
	, wrapped_(std::move(wrapped))
{
	w.logger_.log_u(logmsg::debug_debug, L"Worker %p created new operation %p", &w.owner_, this);
}

void throttled_authenticator::worker::operator()(const event_base &ev)
{
	dispatch<
		operation::result_event,
		util::throttler::permitted_event
	>(ev, this,
		&worker::on_operation_result,
		&worker::on_throttler_permitted_event
	);
}

void throttled_authenticator::worker::on_operation_result(authenticator &, std::unique_ptr<authenticator::operation> &op)
{
	fz::scoped_lock lock(owner_.mutex_);

	authenticating_ = false;

	bool is_none = methods_.size() == 1 && methods_[0].is<method::none>();

	if (!is_none && record_failure_about_failed_none_auth_on_destruction_) {
		logger_.log(logmsg::debug_info, L"A 'none' auth previously failed, but a new auth has been attempted, so no need to record the failure on destruction.");
		record_failure_about_failed_none_auth_on_destruction_ = false;
	}

	if (op->get_error() && op->get_error() != error::internal && !methods_.just_verify()) {
		if (is_none) {
			if (number_of_failed_none_auths_to_report_ == 0) {
				logger_.log(logmsg::debug_info, L"First 'none' authentication attempt failed. Recording the event for later just in case.");
				record_failure_about_failed_none_auth_on_destruction_ = true;
			}
			else
			if (number_of_failed_none_auths_to_report_ == 1) {
				logger_.log(logmsg::debug_info, L"Second 'none' authentication attempt failed. Recording previous failure.");
				record_failure();
				record_failure_about_failed_none_auth_on_destruction_ = false;
			}

			number_of_failed_none_auths_to_report_ += 1;
		}

		if (!is_none || number_of_failed_none_auths_to_report_ > 1) {
			record_failure();
		}
	}

	target_->send_event<operation::result_event>(owner_, std::make_unique<operation>(*this, std::move(op)));
}

void throttled_authenticator::worker::on_throttler_permitted_event()
{
	authenticate();
}

bool throttled_authenticator::worker::try_to_authenticate(const methods_list &methods, std::unique_ptr<authenticator::operation> next_op)
{
	methods_ = methods;
	next_op_ = std::move(next_op);

	owner_.throttler_.wait_until_permitted(family_, ip_, name_, *this);

	return true;
}

bool throttled_authenticator::worker::authenticate()
{
	if (next_op_) {
		if (!authentication::next(std::move(next_op_), methods_))
			return false;
	}
	else {
		logger_.log_u(logmsg::debug_info, L"Authenticating user %s from IP %s.", name_, ip_);
		owner_.wrapped_.authenticate(realm_, name_, methods_, family_, ip_, *this, std::move(meta_for_logging_));
	}

	return authenticating_ = true;
}

void throttled_authenticator::worker::record_failure()
{
	owner_.throttler_.record_failure(family_, ip_, name_);
}

throttled_authenticator::throttled_authenticator(fz::event_loop &loop, fz::authentication::authenticator &wrapped, logger_interface &logger, options opts)
	: event_loop_(loop)
	, wrapped_(wrapped)
	, logger_(logger, "Throttled Authenticator")
	, throttler_(loop, logger_, "Authentication", std::move(opts))
{}

throttled_authenticator::~throttled_authenticator()
{
	workers_.clear();
}

void throttled_authenticator::authenticate(std::string_view realm, std::string_view name, const methods_list &methods, address_type family, std::string_view ip, fz::event_handler &target, logger::modularized::meta_map meta_for_logging)
{
	fz::scoped_lock lock(mutex_);

	auto &worker = workers_.emplace_front(*this, realm, name, family, ip, &target, std::move(meta_for_logging));
	worker.self_in_workers_ = workers_.begin();

	worker.try_to_authenticate(methods);
}

void throttled_authenticator::stop_ongoing_authentications(event_handler &target)
{
	{
		workers_t to_remove;

		{
			fz::scoped_lock lock(mutex_);

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

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

				it = next;
			}
		}
	}

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

void throttled_authenticator::set_options(throttled_authenticator::options opts)
{
	fz::scoped_lock lock(mutex_);

	throttler_.set_options(std::move(opts));
}

}
