#include <cassert>

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

namespace fz::util {

bool throttler::failures_t::add(duration delay, duration cap, std::size_t max_failures, duration failures_window)
{
	auto now = monotonic_clock::now();

	if (max_failures > 0) {
		purge_old(now, failures_window);

		if (timepoints_.size() < max_failures)
			timepoints_.push_back(now);
		else
			timepoints_.back() = now;
	}

	return set_next_try(now, delay, cap, max_failures);
}

bool throttler::failures_t::purge_old(monotonic_clock now, duration failures_window)
{
	if (timepoints_.size()) {
		auto oldest_useful_timepoint = (monotonic_clock(now) -= failures_window);

		do {
			if (oldest_useful_timepoint < timepoints_.front())
				break;

			timepoints_.pop_front();
		} while (timepoints_.size());
	}

	return timepoints_.empty();
}

const monotonic_clock &throttler::failures_t::next_try() const
{
	return next_try_;
}

bool throttler::failures_t::set_next_try(monotonic_clock now, duration delay, duration cap, std::size_t max_failures)
{
	auto must_be_delayed = timepoints_.size() >= max_failures;

	if (must_be_delayed)
		next_try_ = std::min(std::max(next_try_, now) + delay, now + cap);
	else
		next_try_ = now;

	return must_be_delayed;
}

void throttler::record_failure(logger_interface &logger, address_type family, std::string_view ip, std::string_view name)
{
	if (name.empty()) {
		logger.log_u(logmsg::debug_info, L"Recording failed %s from IP %s.", operation_name_, ip);
	}
	else {
		logger.log_u(logmsg::debug_info, L"Recording failed %s for user `%s' from IP %s.", operation_name_, name, ip);
	}

	auto add_failure = [&](auto &map, const auto &key) -> duration {
		auto &f = map[key];
		bool must_be_delayed = f.add(opts_.delay(), opts_.cap(), opts_.max_failures(), opts_.failures_window());

		if (must_be_delayed) {
			return f.next_try() - monotonic_clock::now();
		}

		return {};
	};

	if (!name.empty()) {
		if (auto delta = add_failure(names_failures_, std::string(name)))  {
			logger.log_u(logmsg::debug_warning, L"%s for `%s' has failed too many times (>= %d) within a %ds time window. Next attempt will be delayed %ds from now.",
						  operation_name_, name, opts_.max_failures(), opts_.failures_window().get_seconds(), delta.get_seconds());
		}
	}

	if (family == address_type::ipv4) {
		util::parseable_range r(ip);

		if (hostaddress::ipv4_host h; parse_ip(r, h) && eol(r)) {
			if (auto delta = add_failure(ipv4_failures_, h.to_uint32())) {
				logger.log_u(logmsg::debug_warning, L"%s from IP %s has failed too many times (>= %d) within a %ds time window. Next attempt will be delayed %ds from now.",
							  operation_name_, ip, opts_.max_failures(), opts_.failures_window().get_seconds(), delta.get_seconds());
			}
		}
	}
	else
	if (family == address_type::ipv6) {
		util::parseable_range r(ip);

		if (hostaddress::ipv6_host h; parse_ip(r, h) && eol(r)) {
			if (auto delta = add_failure(ipv6_failures_, h.high_to_uint64())) {
				logger.log_u(logmsg::debug_warning, L"Login from IP %s has failed too many times (>= %d) within a %ds time window. Next attempt will be delayed %ds from now.",
							  operation_name_, ip, opts_.max_failures(), opts_.failures_window().get_seconds(), delta.get_seconds());
			}
		}
	}
	else {
		logger.log_u(logmsg::error, L"Internal error: wrong IP family type %d.", family);
	}

	if (purging_timer_id_ == 0) {
		purging_timer_id_ = add_timer(opts_.failures_window(), true);
	}
}

throttler::throttler(fz::event_loop &loop, logger_interface &logger, std::string_view operation_name, options opts)
	: event_handler(loop)
	, logger_(logger, "Throttler")
	, operation_name_(operation_name)
{
	set_options(std::move(opts));
}

throttler::~throttler()
{
	remove_handler();
}

void throttler::wait_until_permitted(logger_interface &logger, address_type family, std::string_view ip, std::string_view name, receiver_handle<permitted_event> on_permitted)
{
	fz::scoped_lock lock(mutex_);

	auto now = monotonic_clock::now();
	auto next_try = now;

	auto update_next_try = [&next_try, &now, this](auto &map, const auto &key) {
		if (auto it = map.find(key); it != map.end()) {
			if (next_try < it->second.next_try()) {
				next_try = it->second.next_try();
			}

			if (now < it->second.next_try()) {
				it->second.set_next_try(now, opts_.delay(), opts_.cap(), opts_.max_failures());
			}
		}
	};

	if (family == address_type::ipv4) {
		util::parseable_range r(ip);

		if (hostaddress::ipv4_host h; parse_ip(r, h) && eol(r))
			update_next_try(ipv4_failures_, h.to_uint32());
	}
	else
	if (family == address_type::ipv6) {
		util::parseable_range r(ip);

		if (hostaddress::ipv6_host h; parse_ip(r, h) && eol(r))
			update_next_try(ipv6_failures_, h.high_to_uint64());
	}

	if (!name.empty()) {
		update_next_try(names_failures_, std::string(name));
	}

	if (next_try == now) {
		on_permitted();
		return;
	}

	auto delta = next_try - now;

	if (name.empty()) {
		logger.log_u(logmsg::debug_warning, L"%s from IP %s will be delayed %ds.", operation_name_, ip, delta.get_seconds());
	}
	else {
		logger.log_u(logmsg::debug_warning, L"%s for user `%s' from IP %s will be delayed %ds.", operation_name_, name, ip, delta.get_seconds());
	}

	waiting_handlers_.emplace(next_try, std::move(on_permitted));

	if (wait_timer_id_ == 0) {
		wait_timer_id_ = add_timer(delta, true);
	}
}

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

	opts_ = std::move(opts);
}

void throttler::wait_until_permitted(address_type family, std::string_view ip, std::string_view name, receiver_handle<permitted_event> on_permitted)
{
	wait_until_permitted(logger_, family, ip, name, std::move(on_permitted));
}

void throttler::record_failure(address_type family, std::string_view ip, std::string_view name)
{
	record_failure(logger_, family, ip, name);
}

void throttler::operator()(const event_base &ev)
{
	fz::dispatch<timer_event>(ev, [&](timer_id id) {
		auto now = fz::monotonic_clock::now();

		fz::scoped_lock lock(mutex_);

		if (id == wait_timer_id_) {
			wait_timer_id_ = 0;

			logger_.log_u(logmsg::debug_debug, L"Number of waiting handlers before cycle: %d.", waiting_handlers_.size());
			while (!waiting_handlers_.empty()) {
				auto it = waiting_handlers_.begin();

				if (now < it->first) {
					auto delta = it->first - now;
					logger_.log_u(logmsg::debug_debug, L"Next event will be delivered in %ds.", delta.get_seconds());
					wait_timer_id_ = add_timer(delta, true);

					break;
				}

				it->second();
				waiting_handlers_.erase(it);
			}

			logger_.log_u(logmsg::debug_debug, L"Number of waiting handlers after cycle: %d.", waiting_handlers_.size());
		}
		else
		if (id == purging_timer_id_) {
			purging_timer_id_ = 0;

			auto purge = [&](auto &map, const wchar_t *name) {
				if (map.empty()) {
					return;
				}

				logger_.log_u(logmsg::debug_debug, L"Number of %s failures before purging cycle: %d.", name, map.size());

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

					if (it->second.purge_old(now, opts_.failures_window()))
						map.erase(it);

					it = next;
				}
				logger_.log_u(logmsg::debug_debug, L"Number of %s failures after purging cycle: %d.", name, map.size());
			};

			purge(ipv4_failures_, L"IPv4");
			purge(ipv6_failures_, L"IPv6");
			purge(names_failures_, L"names");

			if (!names_failures_.empty() || !ipv4_failures_.empty() || !ipv6_failures_.empty()) {
				purging_timer_id_ = add_timer(opts_.failures_window(), true);
			}
		}
	});
}

throttler::with_new_logger::with_new_logger(throttler &t, logger_interface &logger)
	: t_(t)
	, logger_(logger, "Throttler")
{
}

void throttler::with_new_logger::wait_until_permitted(address_type family, std::string_view ip, std::string_view name, receiver_handle<permitted_event> on_permitted)
{
	t_.wait_until_permitted(logger_, family, ip, name, std::move(on_permitted));
}

void throttler::with_new_logger::record_failure(address_type family, std::string_view ip, std::string_view name)
{
	t_.record_failure(logger_, family, ip, name);
}

}
