#ifndef FZ_UTIL_THROTTLER_HPP
#define FZ_UTIL_THROTTLER_HPP

#include <unordered_map>
#include <map>
#include <deque>

#include <libfilezilla/event_handler.hpp>
#include <libfilezilla/iputils.hpp>

#include "../logger/modularized.hpp"
#include "options.hpp"
#include "utf8_name_map.hpp"
#include "../receiver/handle.hpp"

namespace fz::util
{

class throttler: private event_handler
{
public:
	using permitted_event = receiver_event<throttler>;

	struct options: util::options<options, throttler>
	{
		opt<std::size_t> max_failures    = o(5);
		opt<duration>    failures_window = o(duration::from_seconds(60));
		opt<duration>    delay           = o(duration::from_seconds(5));
		opt<duration>    cap             = o(duration::from_seconds(60));

		options(){}
	};

	throttler(event_loop &loop, logger_interface &logger, std::string_view operation_name, options opts = {});
	~throttler() override;

	void set_options(options opts);

	void wait_until_permitted(address_type family, std::string_view ip, std::string_view name, receiver_handle<permitted_event> on_permitted);
	void record_failure(address_type family, std::string_view ip, std::string_view name);

	class with_new_logger;

private:
	void wait_until_permitted(logger_interface &logger, address_type family, std::string_view ip, std::string_view name, receiver_handle<permitted_event> on_permitted);
	void record_failure(logger_interface &logger, address_type family, std::string_view ip, std::string_view name);

	class worker;

	logger::modularized logger_;

	mutable mutex mutex_;

private:
	void operator()(const event_base &ev) override;

	fz::timer_id wait_timer_id_{};
	fz::timer_id purging_timer_id_{};

private:
	class failures_t
	{
	public:
		bool add(duration delay, duration cap, std::size_t max_failures, duration failures_window);
		bool purge_old(monotonic_clock now, duration failures_window);
		bool set_next_try(monotonic_clock now, duration delay, duration cap, std::size_t max_failures);
		const monotonic_clock &next_try() const;

	private:
		std::deque<monotonic_clock> timepoints_;
		fz::monotonic_clock next_try_;
	};

	using waiting_handlers_t = std::multimap<fz::monotonic_clock, receiver_handle<permitted_event>>;
	using ipv4_failures_t = std::unordered_map<std::uint32_t, failures_t>;
	using ipv6_failures_t = std::unordered_map<std::uint64_t, failures_t>;
	using names_failures_t = utf8_name_map<failures_t>;

	waiting_handlers_t waiting_handlers_;
	ipv4_failures_t ipv4_failures_;
	ipv6_failures_t ipv6_failures_;
	names_failures_t names_failures_;

	std::string operation_name_;
	options opts_;
};

class throttler::with_new_logger
{
public:
	with_new_logger(throttler &t, logger_interface &logger);

	void wait_until_permitted(address_type family, std::string_view ip, std::string_view name, receiver_handle<permitted_event> on_permitted);
	void record_failure(address_type family, std::string_view ip, std::string_view name);

private:
	throttler &t_;
	logger::modularized logger_;
};

}

#endif // FZ_UTIL_THROTTLER_HPP
