/*

Copyright (c) 2004-2010, 2012-2021, Arvid Norberg
Copyright (c) 2016-2017, 2020-2021, Alden Torres
Copyright (c) 2017, Steven Siloti
Copyright (c) 2020, Paul-Louis Ageneau
All rights reserved.

You may use, distribute and modify this code under the terms of the BSD license,
see LICENSE file.
*/

#include <algorithm>
#include <cctype>
#include <iterator>

#include "libtorrent/aux_/io.hpp"
#include "libtorrent/aux_/session_interface.hpp"
#include "libtorrent/aux_/session_settings.hpp"
#include "libtorrent/aux_/http_tracker_connection.hpp"
#include "libtorrent/performance_counters.hpp"
#include "libtorrent/aux_/socket_io.hpp"
#include "libtorrent/aux_/ssl.hpp"
#include "libtorrent/aux_/tracker_manager.hpp"
#include "libtorrent/aux_/udp_tracker_connection.hpp"

#if TORRENT_USE_RTC
#include "libtorrent/aux_/websocket_tracker_connection.hpp"
#endif

using namespace std::placeholders;

namespace libtorrent::aux {

	timeout_handler::timeout_handler(io_context& ios)
		: m_start_time(clock_type::now())
		, m_read_time(m_start_time)
		, m_timeout(ios)
	{}

	timeout_handler::~timeout_handler()
	{
		TORRENT_ASSERT(m_outstanding_timer_wait == 0);
	}

	void timeout_handler::set_timeout(int completion_timeout, int read_timeout)
	{
		m_completion_timeout = completion_timeout;
		m_read_timeout = read_timeout;
		m_start_time = m_read_time = clock_type::now();

		TORRENT_ASSERT(completion_timeout > 0 || read_timeout > 0);

		if (m_abort) return;

		int timeout = 0;
		if (m_read_timeout > 0) timeout = m_read_timeout;
		if (m_completion_timeout > 0)
		{
			timeout = timeout == 0
				? m_completion_timeout
				: std::min(m_completion_timeout, timeout);
		}

		ADD_OUTSTANDING_ASYNC("timeout_handler::timeout_callback");
		m_timeout.expires_at(m_read_time + seconds(timeout));
		m_timeout.async_wait(std::bind(
			&timeout_handler::timeout_callback, shared_from_this(), _1));
#if TORRENT_USE_ASSERTS
		++m_outstanding_timer_wait;
#endif
	}

	void timeout_handler::restart_read_timeout()
	{
		m_read_time = clock_type::now();
	}

	void timeout_handler::cancel()
	{
		m_abort = true;
		m_completion_timeout = 0;
		m_timeout.cancel();
	}

	void timeout_handler::timeout_callback(error_code const& error)
	{
		COMPLETE_ASYNC("timeout_handler::timeout_callback");
#if TORRENT_USE_ASSERTS
		TORRENT_ASSERT(m_outstanding_timer_wait > 0);
		--m_outstanding_timer_wait;
#endif
		if (m_abort) return;

		time_point now = clock_type::now();
		time_duration receive_timeout = now - m_read_time;
		time_duration completion_timeout = now - m_start_time;

		if ((m_read_timeout
				&& m_read_timeout <= total_seconds(receive_timeout))
			|| (m_completion_timeout
				&& m_completion_timeout <= total_seconds(completion_timeout))
			|| error)
		{
			on_timeout(error);
			return;
		}

		int timeout = 0;
		if (m_read_timeout > 0) timeout = m_read_timeout;
		if (m_completion_timeout > 0)
		{
			timeout = timeout == 0
				? int(m_completion_timeout - total_seconds(m_read_time - m_start_time))
				: std::min(int(m_completion_timeout - total_seconds(m_read_time - m_start_time)), timeout);
		}
		ADD_OUTSTANDING_ASYNC("timeout_handler::timeout_callback");
		m_timeout.expires_at(m_read_time + seconds(timeout));
		m_timeout.async_wait(
			std::bind(&timeout_handler::timeout_callback, shared_from_this(), _1));
#if TORRENT_USE_ASSERTS
		++m_outstanding_timer_wait;
#endif
	}

	tracker_connection::tracker_connection(
		tracker_manager& man
		, tracker_request req
		, io_context& ios
		, std::weak_ptr<request_callback> r)
		: timeout_handler(ios)
		, m_req(std::move(req))
		, m_requester(std::move(r))
		, m_man(man)
	{}

	std::shared_ptr<request_callback> tracker_connection::requester() const
	{
		return m_requester.lock();
	}

	void tracker_connection::fail(error_code const& ec, operation_t const op
		, char const* msg, seconds32 const interval, seconds32 const min_interval)
	{
		// we need to post the error to avoid deadlock
		post(get_executor(), std::bind(&tracker_connection::fail_impl
			, shared_from_this(), ec, op, std::string(msg), interval, min_interval));
	}

	void tracker_connection::fail_impl(error_code const& ec, operation_t const op
		, std::string const msg, seconds32 const interval, seconds32 const min_interval)
	{
		// fail() only posts this call, so m_req is not necessarily still
		// pending by the time it runs: the connection's own logic may have
		// already reported an outcome for it (e.g. a legitimate response
		// arriving right as tracker_manager::abort_all_requests() posted
		// this same call). Checking here, rather than in fail(), is what
		// makes the two mutually exclusive.
		if (m_req_pending)
		{
			m_req_pending = false;
			std::shared_ptr<request_callback> cb = requester();
			if (cb)
				cb->tracker_request_error(
					m_req, ec, op, msg, interval.count() == 0 ? min_interval : interval);
		}
		close();
	}

	void tracker_connection::sent_bytes(int bytes)
	{
		m_man.sent_bytes(bytes);
	}

	void tracker_connection::received_bytes(int bytes)
	{
		m_man.received_bytes(bytes);
	}

	tracker_manager::tracker_manager(send_fun_t send_fun
		, send_fun_hostname_t send_fun_hostname
		, counters& stats_counters
		, aux::resolver_interface& resolver
		, aux::session_settings const& sett
#if !defined TORRENT_DISABLE_LOGGING || TORRENT_USE_ASSERTS
		, aux::session_logger& ses
#endif
		)
		: m_send_fun(std::move(send_fun))
		, m_send_fun_hostname(std::move(send_fun_hostname))
		, m_host_resolver(resolver)
		, m_settings(sett)
		, m_stats_counters(stats_counters)
#if !defined TORRENT_DISABLE_LOGGING || TORRENT_USE_ASSERTS
		, m_ses(ses)
#endif
	{}

	tracker_manager::~tracker_manager()
	{
		abort_all_requests(true);
	}

	void tracker_manager::sent_bytes(int bytes)
	{
		TORRENT_ASSERT(m_ses.is_single_thread());
		m_stats_counters.inc_stats_counter(counters::sent_tracker_bytes, bytes);
	}

	void tracker_manager::received_bytes(int bytes)
	{
		TORRENT_ASSERT(m_ses.is_single_thread());
		m_stats_counters.inc_stats_counter(counters::recv_tracker_bytes, bytes);
	}

	void tracker_manager::remove_request(aux::http_tracker_connection const* c)
	{
		TORRENT_ASSERT(is_single_thread());
		auto const i = std::find_if(m_http_conns.begin(), m_http_conns.end()
			, [c] (std::shared_ptr<aux::http_tracker_connection> const& ptr) { return ptr.get() == c; });
		if (i != m_http_conns.end())
		{
			m_http_conns.erase(i);
			if (!m_queued.empty())
			{
				auto conn = std::move(m_queued.front());
				m_queued.pop_front();
				m_http_conns.push_back(std::move(conn));
				m_http_conns.back()->start();
				m_stats_counters.set_value(counters::num_queued_tracker_announces, std::int64_t(m_queued.size()));
			}
			return;
		}

		auto const j = std::find_if(m_queued.begin(), m_queued.end()
			, [c] (std::shared_ptr<aux::http_tracker_connection> const& ptr) { return ptr.get() == c; });
		if (j != m_queued.end())
		{
			m_queued.erase(j);
			m_stats_counters.set_value(counters::num_queued_tracker_announces, std::int64_t(m_queued.size()));
		}
	}

	void tracker_manager::remove_request(aux::udp_tracker_connection const* c)
	{
		TORRENT_ASSERT(is_single_thread());
		m_udp_conns.erase(c->transaction_id());
	}

#if TORRENT_USE_RTC
	void tracker_manager::remove_request(aux::websocket_tracker_connection const* c)
	{
		TORRENT_ASSERT(is_single_thread());
		tracker_request const& req = c->tracker_req();
		auto const it = m_websocket_conns.find(req.url);
		if (it != m_websocket_conns.end() && it->second.get() == c)
			m_websocket_conns.erase(it);
	}
#endif

	void tracker_manager::update_transaction_id(
		std::shared_ptr<aux::udp_tracker_connection> c
		, std::uint32_t tid)
	{
		TORRENT_ASSERT(is_single_thread());
		m_udp_conns.erase(c->transaction_id());
		m_udp_conns[tid] = std::move(c);
	}

	void tracker_manager::queue_request(
		io_context& ios
		, tracker_request&& req
		, aux::session_settings const& sett
		, std::weak_ptr<request_callback> c)
	{
		TORRENT_ASSERT(is_single_thread());
		TORRENT_ASSERT(req.num_want >= 0);
		TORRENT_ASSERT(!m_abort || req.event == event_t::stopped);
		if (m_abort && req.event != event_t::stopped) return;
		bool const high_priority = bool(req.kind & tracker_request::high_priority);

#ifndef TORRENT_DISABLE_LOGGING
		if (auto cb = c.lock())
		{
			cb->debug_log("*** QUEUE_TRACKER_REQUEST [ listen_port: %d priority: %d ]"
				, req.listen_port, high_priority);
		}
#endif

		std::string const protocol = req.url.substr(0, req.url.find(':'));

#if TORRENT_USE_SSL
		if (protocol == "http" || protocol == "https")
#else
		if (protocol == "http")
#endif
		{
			auto con = std::make_shared<aux::http_tracker_connection>(ios, *this, std::move(req), c);
			if (m_http_conns.size() < std::size_t(sett.get_int(settings_pack::max_concurrent_http_announces)))
			{
				m_http_conns.push_back(std::move(con));
				m_http_conns.back()->start();
			}
			else
			{
				if (high_priority)
				{
					m_queued.push_front(std::move(con));
				}
				else
				{
					m_queued.push_back(std::move(con));
				}
				m_stats_counters.set_value(counters::num_queued_tracker_announces, std::int64_t(m_queued.size()));
			}
			return;
		}
		else if (protocol == "udp")
		{
			auto con = std::make_shared<aux::udp_tracker_connection>(ios, *this, std::move(req), c);
			m_udp_conns[con->transaction_id()] = con;
			con->start();
			return;
        }
#if TORRENT_USE_RTC
        else if (protocol == "ws" || protocol == "wss")
        {
			std::shared_ptr<request_callback> cb = c.lock();
			if (!cb) return;

			int const max_offers = std::max(sett.get_int(settings_pack::max_webtorrent_offers), 0);
			req.num_want = std::min(req.num_want, max_offers);
			if (req.num_want == 0)
			{
				// when we're shutting down, we don't really want to
				// re-establish the persistent websocket connection just to
				// announce "stopped", and advertise 0 offers. It may hang
				// shutdown.
				post(ios, std::bind(&request_callback::tracker_request_error, cb, std::move(req)
					, errors::torrent_aborted, operation_t::connect
					, "", seconds32(0)));
				return;
			}
			cb->generate_rtc_offers(req.num_want
				, [this, &ios, request = std::move(req), c](error_code const& ec
					, std::vector<aux::rtc_offer> offers) mutable
			{
				if (!ec) request.offers = std::move(offers);

				// m_abort may have been set (session shutdown) while this
				// offer generation was in flight; the guard at the top of
				// queue_request() only protects requests queued before
				// m_abort was set, so re-check here before (re-)establishing
				// a persistent connection, for the same reason the
				// num_want==0 branch above avoids doing so during shutdown.
				if (m_abort && request.event != event_t::stopped)
				{
					if (auto rc = c.lock())
						post(ios,
							std::bind(&request_callback::tracker_request_error,
								rc,
								std::move(request),
								errors::torrent_aborted,
								operation_t::connect,
								"",
								seconds32(0)));
					return;
				}

				auto it = m_websocket_conns.find(request.url);
				if (it != m_websocket_conns.end() && it->second->is_started()) {
					it->second->queue_request(std::move(request), c);
				} else {
					auto con = std::make_shared<aux::websocket_tracker_connection>(
							ios, *this, std::move(request), c);
					con->start();
					m_websocket_conns[request.url] = con;
				}
			});
			return;
        }
#endif
		// we need to post the error to avoid deadlock
		else if (auto r = c.lock())
			post(ios, std::bind(&request_callback::tracker_request_error, r, std::move(req)
				, errors::unsupported_url_protocol, operation_t::parse_address
				, "", seconds32(0)));
	}

	bool tracker_manager::incoming_packet(udp::endpoint const& ep
		, span<char const> const buf)
	{
		TORRENT_ASSERT(is_single_thread());
		// ignore packets smaller than 8 bytes
		if (buf.size() < 8)
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (m_ses.should_log())
			{
				m_ses.session_log("incoming packet from %s, not a UDP tracker message "
					"(%d Bytes)", print_endpoint(ep).c_str(), int(buf.size()));
			}
#endif
			return false;
		}

		// the first word is the action, if it's not [0, 3]
		// it's not a valid udp tracker response
		span<const char> ptr = buf;
		std::uint32_t const action = aux::read_uint32(ptr);
		if (action > 3) return false;

		std::uint32_t const transaction = aux::read_uint32(ptr);
		auto const i = m_udp_conns.find(transaction);

		if (i == m_udp_conns.end())
		{
#ifndef TORRENT_DISABLE_LOGGING
			if (m_ses.should_log())
			{
				m_ses.session_log("incoming UDP tracker packet from %s has invalid "
					"transaction ID (%x)", print_endpoint(ep).c_str()
					, transaction);
			}
#endif
			return false;
		}

		auto const p = i->second;
		// on_receive() may remove the tracker connection from the list
		return p->on_receive(ep, buf);
	}

	void tracker_manager::incoming_error(error_code const&
		, udp::endpoint const&)
	{
		TORRENT_ASSERT(is_single_thread());
		// TODO: 2 implement
	}

	bool tracker_manager::incoming_packet(string_view const hostname
		, span<char const> const buf)
	{
		TORRENT_ASSERT(is_single_thread());
		// ignore packets smaller than 8 bytes
		if (buf.size() < 16) return false;

		// the first word is the action, if it's not [0, 3]
		// it's not a valid udp tracker response
		span<const char> ptr = buf;
		std::uint32_t const action = aux::read_uint32(ptr);
		if (action > 3) return false;

		std::uint32_t const transaction = aux::read_uint32(ptr);
		auto const i = m_udp_conns.find(transaction);

		if (i == m_udp_conns.end())
		{
#ifndef TORRENT_DISABLE_LOGGING
			// now, this may not have been meant to be a tracker response,
			// but chances are pretty good, so it's probably worth logging
			m_ses.session_log("incoming UDP tracker packet from %s has invalid "
				"transaction ID (%x)", std::string(hostname).c_str(), transaction);
#endif
			return false;
		}

		auto const p = i->second;
		// on_receive() may remove the tracker connection from the list
		return p->on_receive_hostname(hostname, buf);
	}

	void tracker_manager::send_hostname(aux::listen_socket_handle const& sock
		, char const* hostname, int const port
		, span<char const> p, error_code& ec, aux::udp_send_flags_t const flags)
	{
		TORRENT_ASSERT(is_single_thread());
		m_send_fun_hostname(sock, hostname, port, p, ec, flags);
	}

	void tracker_manager::send(aux::listen_socket_handle const& sock
		, udp::endpoint const& ep
		, span<char const> p
		, error_code& ec, aux::udp_send_flags_t const flags)
	{
		TORRENT_ASSERT(is_single_thread());
		m_send_fun(sock, ep, p, ec, flags);
	}

	void tracker_manager::stop()
	{
		abort_all_requests();
		m_abort = true;
	}

	void tracker_manager::abort_all_requests(bool all)
	{
		// this is called from the destructor too, which is not subject to the
		// single-thread requirement.
		TORRENT_ASSERT(all || is_single_thread());
		// removes all connections except 'event=stopped'-requests. Aborted
		// requests are reported as a skipped announce, which clears the
		// "updating" flag on the requester's end, so the tracker is
		// eligible to be announced to again, e.g. once a paused session is
		// resumed.

		std::vector<std::shared_ptr<aux::http_tracker_connection>> close_http_queued;
		std::vector<std::shared_ptr<aux::http_tracker_connection>> close_http_started;
		std::vector<std::shared_ptr<aux::udp_tracker_connection>> close_udp_connections;

		// remove queued (not yet started) requests from the queue right
		// away, rather than leaving that to the closing loop below.
		// Otherwise, closing one of the started connections below would
		// dequeue and start the next queued request, just to abort it
		// again immediately after.
		auto const erase_begin = std::remove_if(m_queued.begin(),
			m_queued.end(),
			[&](std::shared_ptr<aux::http_tracker_connection>& c) {
				tracker_request const& req = c->tracker_req();
				if (req.event == event_t::stopped && !all) return false;

#ifndef TORRENT_DISABLE_LOGGING
				std::shared_ptr<request_callback> rc = c->requester();
				if (rc) rc->debug_log("aborting: %s", req.url.c_str());
#endif
				close_http_queued.push_back(std::move(c));
				return true;
			});
		m_queued.erase(erase_begin, m_queued.end());
		m_stats_counters.set_value(
			counters::num_queued_tracker_announces, std::int64_t(m_queued.size()));

		for (auto const& c : m_http_conns)
		{
			tracker_request const& req = c->tracker_req();
			if (req.event == event_t::stopped && !all)
				continue;

			close_http_started.push_back(c);

#ifndef TORRENT_DISABLE_LOGGING
			std::shared_ptr<request_callback> rc = c->requester();
			if (rc) rc->debug_log("aborting: %s", req.url.c_str());
#endif
		}
		for (auto const& p : m_udp_conns)
		{
			auto const& c = p.second;
			tracker_request const& req = c->tracker_req();
			if (req.event == event_t::stopped && !all)
				continue;

			close_udp_connections.push_back(c);

#ifndef TORRENT_DISABLE_LOGGING
			std::shared_ptr<request_callback> rc = c->requester();
			if (rc) rc->debug_log("aborting: %s", req.url.c_str());
#endif
		}

		// tearing everything down for good (the tracker_manager destructor)
		// reports no outcome and closes synchronously: posting here would
		// be unsafe, since the io_context is not guaranteed to run again
		// before this object is destroyed. Otherwise, report each as a
		// skipped announce (fail() posts, so this is safe to call while
		// still iterating connection containers here) so the "updating"
		// flag on the torrent's end is cleared and the tracker becomes
		// eligible to be announced to again, e.g. once the session is
		// resumed.
		if (all)
		{
			for (auto const& c : close_http_queued)
				c->close();
			for (auto const& c : close_http_started)
				c->close();
			for (auto const& c : close_udp_connections)
				c->close();
		}
		else
		{
			for (auto const& c : close_http_queued)
				c->fail(errors::announce_skipped, operation_t::bittorrent);
			for (auto const& c : close_http_started)
				c->fail(errors::announce_skipped, operation_t::bittorrent);
			for (auto const& c : close_udp_connections)
				c->fail(errors::announce_skipped, operation_t::bittorrent);
		}

#if TORRENT_USE_RTC
		// close() erases its own entry from this map synchronously (via
		// remove_request()), so capture the next iterator before calling
		// it. Deliberately not collected into a vector first (unlike
		// close_http_started/close_udp_connections above): this function
		// runs from ~tracker_manager() too, whose implicit noexcept means
		// a bad_alloc from that allocation would call std::terminate().
		for (auto it = m_websocket_conns.begin(); it != m_websocket_conns.end();)
		{
			auto const& c = it->second;
			auto const next = std::next(it);

			if (all)
			{
				c->close();
				it = next;
				continue;
			}

			// leaves the connection running if it still has a
			// stopped-event request queued or in flight on it.
			if (c->prune_non_stopped_requests())
			{
				it = next;
				continue;
			}

#ifndef TORRENT_DISABLE_LOGGING
			std::shared_ptr<request_callback> rc = c->requester();
			if (rc)
				rc->debug_log("aborting: %s", c->tracker_req().url.c_str());
#endif
			// fail() alone does not report anything: it suppresses
			// fail_impl()'s single-request report (see its definition) so
			// that close(ec, op), below, is the only thing that reports an
			// outcome, with the right reason, to every multiplexed
			// request. Skipping the close(ec, op) call here would silently
			// fall back to fail_impl()'s deferred, argument-less close(),
			// reporting operation_aborted/unknown instead.
			c->fail(errors::announce_skipped, operation_t::bittorrent);
			c->close(errors::announce_skipped, operation_t::bittorrent);
			it = next;
		}
#endif
	}

	bool tracker_manager::empty() const
	{
		TORRENT_ASSERT(is_single_thread());
		return m_http_conns.empty() && m_udp_conns.empty()
#if TORRENT_USE_RTC
			&& m_websocket_conns.empty()
#endif
			;
	}

	int tracker_manager::num_requests() const
	{
		TORRENT_ASSERT(is_single_thread());
		return int(m_http_conns.size() + m_udp_conns.size()
#if TORRENT_USE_RTC
			+ m_websocket_conns.size()
#endif
			);
	}
}
