#ifndef FZ_AUTHENTICATION_FILE_BASED_AUTHENTICATOR_HPP
#define FZ_AUTHENTICATION_FILE_BASED_AUTHENTICATOR_HPP

#include <unordered_map>

#include <libfilezilla/hash.hpp>
#include <libfilezilla/impersonation.hpp>

#include "../logger/modularized.hpp"
#include "../authentication/authenticator.hpp"
#include "../serialization/types/containers.hpp"
#include "../serialization/types/variant.hpp"
#include "../serialization/types/optional.hpp"
#include "../serialization/types/binary_address_list.hpp"
#include "../serialization/types/fs_path.hpp"
#include "../serialization/types/tvfs.hpp"
#include "../util/traits.hpp"
#include "../util/xml_archiver.hpp"
#include "../util/integral_ops.hpp"
#include "../tvfs/limits.hpp"
#include "../util/copies_counter.hpp"
#include "../bounded_thread_pool.hpp"

#include "realms.hpp"
#include "credentials.hpp"
#include "identity_provider.hpp"
#include "impersonation_context.hpp"

namespace fz::authentication {

class file_based_authenticator: public authenticator
{
public:
	struct rate_limits {
		rate::type inbound{rate::unlimited};
		rate::type outbound{rate::unlimited};
		rate::type session_inbound{rate::unlimited};
		rate::type session_outbound{rate::unlimited};

		template <typename Archive>
		void serialize(Archive &ar) {
			using namespace fz::serialization;

			ar.optional_attribute(with_unlimited{inbound, rate::unlimited}, "inbound")
			  .optional_attribute(with_unlimited{outbound, rate::unlimited}, "outbound")
			  .optional_attribute(with_unlimited{session_inbound, rate::unlimited}, "session_inbound")
			  .optional_attribute(with_unlimited{session_outbound, rate::unlimited}, "session_outbound");
		}
	};

	struct auth_entry {
		tvfs::mount_table mount_table{};
		struct rate_limits rate_limits{};
		tvfs::open_limits session_open_limits{};
		tcp::binary_address_list allowed_ips{};
		tcp::binary_address_list disallowed_ips{};
		std::uint16_t session_count_limit{};

		std::string description{};
		std::vector<realm> realms{};

		template <typename Archive>
		void serialize(Archive &ar) {
			using namespace fz::serialization;

			ar(
				nvp(mount_table, "", "mount_point"),
				optional_nvp(rate_limits, "rate_limits"),
				optional_nvp(allowed_ips, "allowed_ips"),
				optional_nvp(disallowed_ips, "disallowed_ips"),
				optional_nvp(session_open_limits, "session_open_limits"),
				optional_nvp(with_unlimited{session_count_limit, 0}, "session_count_limit"),
				optional_nvp(description, "description"),
				optional_nvp(realms, "", "realm")
			);
		}
	};

	struct group_entry: public auth_entry {
		using auth_entry::auth_entry;

		//TODO: implement C3 linearization
		//std::vector<std::string> groups;
	};

	struct user_entry: public auth_entry {
		using auth_entry::auth_entry;

		bool enabled{true};
		std::vector<std::string> groups{};
		authentication::credentials credentials;
		available_methods methods { credentials.get_most_secure_methods() };

		struct identity_profile
		{
			std::string idp;
			std::string uid;
			identity_provider::parameters parameters;

			template <typename Archive>
			void serialize(Archive &ar)
			{
				ar.attribute(idp, "idp").optional_attribute(uid, "uid")
				(
					serialization::nvp(parameters, "", "param")
				);
			}
		};

		std::optional<identity_profile> identity;

		template <typename Archive>
		void serialize(Archive &ar)
		{
			using namespace fz::serialization;

			ar.optional_attribute(enabled, "enabled")(
				nvp(static_cast<auth_entry &>(*this), ""),
				nvp(groups, "", "group"),
				nvp(credentials, ""),
				optional_nvp(methods, "methods"),
				nvp(identity, "identity")
			);
		}
	};

	struct groups: std::unordered_map<std::string /*name*/, group_entry>
	{
		using unordered_map::unordered_map;

		static inline const std::string invalid_chars_in_name = "<>\r\n\0";

		template <typename Archive>
		void serialize(Archive &ar) {
			using namespace fz::serialization;
			ar(nvp(with_key_name(*this, "name"), "", "group"));
		}
	};

	struct users: users_map<user_entry>
	{
		using users_map<user_entry>::users_map;

		static inline const std::string invalid_chars_in_name = "<>\r\n\0";

		struct impersonator
		{
			struct msw
			{
				bool enabled{false};

				native_string name;
				native_string password;

				template <typename Archive>
				void serialize(Archive &ar) {
					using namespace fz::serialization;
					ar.optional_attribute(enabled, "enabled")(
						nvp(name, "name"),
						nvp(password, "password")
					);
				}

				impersonation_token get_token() const;
			};

			struct nix
			{
				bool enabled{false};

				native_string name;
				native_string group;

				template <typename Archive>
				void serialize(Archive &ar) {
					using namespace fz::serialization;
					ar.optional_attribute(enabled, "enabled")(
						nvp(name, "name"),
						nvp(group, "group")
					);
				}

				impersonation_token get_token() const;
			};

		#ifdef FZ_WINDOWS
			using native = msw;
		#else
			using native = nix;
		#endif

			struct any: std::variant<msw, nix>
			{
				using variant::variant;

				impersonator::msw *msw();
				impersonator::nix *nix();
				impersonator::native *native();

				impersonation_token get_token() const;
			};
		};

		impersonator::any default_impersonator = impersonator::native();

		// If users defer to groups and groups defer to system policies, these are the policies that apply.
		// In case these realms' status is `deferred', then the default policy is to enable the realm.
		std::vector<realm> realms{};

		tvfs::backend::credentials_map mount_credentials;

		template <typename Archive>
		void serialize(Archive &ar) {
			using namespace fz::serialization;
			ar(
				optional_nvp(default_impersonator, "default_impersonator"),
				optional_nvp(realms, "", "realm"),
				optional_nvp(with_key_name(mount_credentials, "native_path"), "", "mount_credentials"),
				nvp(with_key_name(*this, "name"), "", "user")
			);
		}
	};

	struct options: util::options<options>
	{
		/// IDPs are processed in the order they appear in this vector.
		opt<std::vector<std::reference_wrapper<identity_provider>>> idps = o();
		opt<native_string> impersonator_exe = o();
		opt<std::size_t> max_concurrent_auths = o(20);
		opt<duration> auth_wait_timeout = o(duration::from_seconds(20));

		options(){}
	};

	file_based_authenticator(thread_pool &thread_pool, event_loop &event_loop, logger_interface &logger, rate_limit_manager &rlm, options opts = {});
	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 &thread_pool, event_loop &event_loop, logger_interface &logger, rate_limit_manager &rlm, native_string groups_path, groups &&groups, native_string users_path, users &&users, options opts = {});
	~file_based_authenticator() override;

	void set_groups_and_users(groups &&groups, users &&users);
	void get_groups_and_users(groups &groups, users &users);

	identity_provider *get_identity_provider(std::string_view name) const;
	std::vector<std::reference_wrapper<identity_provider>> get_identity_providers() const;
	std::vector<std::string> get_identity_provider_names() const;

	// Creates a temporary user with random username and password.
	// This user will not be saved on file or serialized in any other way
	std::pair<std::string /*name*/, std::string /*password*/> make_temp_user(tvfs::mount_table mt);
	bool remove_temp_user(const std::string &name);

	bool load();
	bool save(util::xml_archiver_base::event_dispatch_mode mode = util::xml_archiver_base::delayed_dispatch);

	void set_save_result_event_handler(fz::event_handler *handler);

	serialization::xml_input_archive::error_t load_into(fz::authentication::file_based_authenticator::groups &groups, fz::authentication::file_based_authenticator::users &users);

	static bool save(const native_string &groups_path, const groups &groups, const native_string &users_path, const users &users);

	void 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 = {}) override;
	void stop_ongoing_authentications(event_handler &target) override;

	static bool is_idp_user(const users::value_type &u);
	static std::string username_for_idp(std::string_view idp_name);

private:
	struct idp_data {
		identity_provider *idp;
		std::unordered_map<std::string, users::iterator> managed_users;
	};

	using identity_providers = std::vector<idp_data>;

	idp_data *get_idp_data_by_name(std::string_view name);
	identity_provider *get_id_provider_by_name(std::string_view name);
	void create_idp_user_if_not_existing(const idp_data &idp_data);
	static std::string username_for_idp(const identity_provider &idp);

	struct group_limiters {
		std::shared_ptr<rate_limiter> shared_rate_limiter;
		std::shared_ptr<util::limited_copies_counter> session_count_limiter;
	};

	static authentication::error apply_user_properties(identity_provider::user_properties &&uinfo, user_entry &uentry, logger_interface &logger = get_null_logger());
	void sanitize();

	error check_realm(std::string_view realm, const std::string &name, const user_entry &u);
	void update_shared_user(user &user, const user_entry &entry, logger::modularized &logger_);
	void update_group_limiters(group_limiters &limiters, const groups::value_type &g);
	std::shared_ptr<tvfs::backend> make_shared_tvfs_backend(impersonation_token &token, logger::modularized &logger_);
	shared_user get_or_make_shared_user(const std::string &name, const user_entry &entry, const std::string &id, impersonation_context &&impersonation, logger::modularized &logger_);
	group_limiters &get_or_make_group_limiters(const groups::value_type &g);
	void save_later();
	void update();

	authentication::error get_auth_data_for_user(const std::string& name, std::string &uid, user_entry &uentry, impersonation_context &impersonation, bool &bypass_default_impersonator);

	mutable fz::mutex mutex_{true};

	thread_pool &thread_pool_;
	event_loop &event_loop_;
	logger::modularized logger_;
	rate_limit_manager &rlm_;
	options opts_;
	bounded_thread_pool bounded_thread_pool_;
	async_handler async_handler_;

	class worker;
	using workers = std::list<std::shared_ptr<worker>>;

	fz::mutex workers_mutex_{};
	workers workers_{};

	groups groups_{};
	users users_{};
	users temp_users_{};

	std::unordered_map<std::string, group_limiters> group_limiters_;
	users_map<weak_user> weak_users_map_;

	std::unique_ptr<util::xml_archiver_base> xml_archiver_;

	identity_providers id_providers_;
};

}

FZ_SERIALIZATION_SPECIALIZE_ALL(fz::authentication::file_based_authenticator::groups, unversioned_member_serialize)
FZ_SERIALIZATION_SPECIALIZE_ALL(fz::authentication::file_based_authenticator::users, unversioned_member_serialize)

#endif // FZ_AUTHENTICATION_FILE_BASED_AUTHENTICATOR_HPP
