#include <libfilezilla/util.hpp>

#include "../tcp/server.hpp"

namespace fz::tcp {

server::server(server::context &context, logger_interface &logger, session::factory &session_factory)
	: event_handler(context.loop())
	, context_(context)
	, logger_(logger)
	, session_factory_(session_factory)
	, listeners_loop_(context.pool())
	, listeners_(context.pool(), listeners_loop_, *this, logger_, session_factory_, session_factory_)
{
}

server::~server()
{
	logger_.log_u(logmsg::debug_debug, L"Destroying.");
	remove_handler();
	stop(true);
}

std::size_t server::end_sessions(const std::vector<session::id> &ids, int err)
{
	return iterate_over_sessions(ids, [&](session &s) {
		s.shutdown(err);
		return true;
	});
}

scoped_borrow<session> server::get_session(session::id id)
{
	scoped_lock lock(mutex_);

	session *s = [&]() -> session * {
		auto it = sessions_.find(id);
		if (it != sessions_.end())
			return it->second.get();

		return nullptr;
	}();

	return scoped_borrow<session>(s);
}

bool server::start()
{
	return listeners_.start();
}

bool server::stop(bool destroy_all_sessions)
{
	if (!listeners_.stop()) {
		return false;
	}

	// Session destruction must happen outside mutex, see also on_session_ended_event
	decltype(sessions_) sessions_to_destroy;

	if (destroy_all_sessions) {
		scoped_lock lock(mutex_);

		sessions_to_destroy = std::move(sessions_);
		num_sessions_ = 0;

		for (auto &[_, s]: sessions_to_destroy) {
			s->borrowable::stop_borrowing();
		}

		logger_.log_u(logmsg::debug_debug, L"Waiting for all sessions' borrows to be released");
		for (auto &[_, s]: sessions_to_destroy) {
			s->borrowable::wait_until_no_active_borrows();
		}

		logger_.log_u(logmsg::debug_debug, L"Sessions have no outstanding borrows anymore.");
		logger_.log_u(logmsg::debug_debug, L"Destroying sessions.");
	}

	return true;
}

bool server::is_running() const
{
	return listeners_.is_running();
}

void server::operator ()(const event_base &ev)
{
	fz::dispatch<
		tcp::listener::connected_event,
		typename session::ended_event
	>(ev, this,
		&server::on_connected_event,
		&server::on_session_ended_event
	);
}

void server::on_connected_event(tcp::listener &listener)
{
	std::size_t loop_count = 10;

	while (auto socket = listener.get_socket()) {
		int error = 0;
		auto id = context_.next_session_id();
		auto session = session_factory_.make_session(*this, id, std::move(socket), listener.get_user_data(), error);

		if (session) {
			scoped_lock lock(mutex_);
			sessions_.insert({id, std::move(session)});
			num_sessions_ += 1;
		}

		// Don't starve the event loop
		if (--loop_count == 0) {
			if (listener.has_socket()) {
				send_event<listener::connected_event>(listener);
				break;
			}
		}
	}
}

void server::on_session_ended_event(session::id id, const channel::error_type &error)
{
	decltype(sessions_)::node_type extracted;

	{
		scoped_lock lock(mutex_);
		num_sessions_ -= 1;
		extracted = sessions_.extract(id);
	}

	if (!extracted)
		return;

	if (!error)
		logger_.log_u(logmsg::status, "Session %d ended gracefully.", id);
	else
	if (session_factory_.log_on_session_exit() && error != ECONNRESET)
		logger_.log_u(logmsg::error, "Session %d ended with error %d. Reason: %s.", id, error.error(), socket_error_description(error.error()));

	extracted.mapped()->borrowable::stop_borrowing();

	logger_.log_u(logmsg::debug_debug, L"Waiting for all session %d's borrows to be released.", id);
	extracted.mapped()->borrowable::wait_until_no_active_borrows();
	logger_.log_u(logmsg::debug_debug, L"Session %d has no outstanding borrows anymore.", id);
}

const std::vector<session::id> &server::get_session_ids() const
{
	// Using a thread_local allows to amortize the cost of allocations
	thread_local std::vector<session::id> static_ids;

	scoped_lock lock(mutex_);

	static_ids.clear();
	static_ids.reserve(sessions_.size());

	for (auto &[id, _]: sessions_) {
		static_ids.push_back(id);
	}

	return static_ids;
}

}
