/*
* Copyright (c) 2013-2026, The PurpleI2P Project
*
* This file is part of Purple i2pd project and licensed under BSD3
*
* See full license text in LICENSE file at top of project tree
*/

#include "Destination.h"
#include "Identity.h"
#include "ClientContext.h"
#include "I2PService.h"
#include <boost/asio/error.hpp>

namespace i2p
{
namespace client
{
	static const i2p::data::SigningKeyType I2P_SERVICE_DEFAULT_KEY_TYPE = i2p::data::SIGNING_KEY_TYPE_EDDSA_SHA512_ED25519;

	I2PService::I2PService (std::shared_ptr<ClientDestination> localDestination):
		m_LocalDestination (localDestination ? localDestination :
			i2p::client::context.CreateNewLocalDestination (false, I2P_SERVICE_DEFAULT_KEY_TYPE)),
			m_ReadyTimer(m_LocalDestination->GetService()), m_ReadyTimerTriggered(false),
			m_ConnectTimeout(0), m_CloseIdleTime (0), m_NewDestOnResume (false), m_LastActivityTime (0),
			isUpdated (true)
	{
		m_LocalDestination->Acquire ();
	}

	I2PService::I2PService (i2p::data::SigningKeyType kt):
		I2PService (i2p::client::context.CreateNewLocalDestination (false, kt))
	{
	}

	I2PService::~I2PService ()
	{
		ClearHandlers ();
		if (m_LocalDestination) m_LocalDestination->Release ();
	}

	void I2PService::Start ()
	{
		if (m_CloseIdleTime)
		{
			if (m_LocalDestination && m_LocalDestination->GetRefCounter () <= 1)
			{
				if (!m_IdleCheckTimer) m_IdleCheckTimer.reset (new boost::asio::steady_timer(m_LocalDestination->GetService ()));
				m_LastActivityTime = i2p::util::GetMonotonicMilliseconds ();
				ScheduleIdleCheckTimer ();
			}
			else
				LogPrint (eLogError, "I2PService: i2cp.closeIdleTime can't be set for ", GetName (), " on shared destination");
		}
	}

	void I2PService::Stop ()
	{
		if (m_IdleCheckTimer)
		{
			m_IdleCheckTimer->cancel ();
			m_IdleCheckTimer = nullptr;
		}
	}

	void I2PService::ClearHandlers ()
	{
		if(m_ConnectTimeout)
			m_ReadyTimer.cancel();
		IterateHandlers ([](std::shared_ptr<I2PServiceHandler> handler)
			{
				if (handler) handler->Terminate ();
			});
		m_Handlers.clear();
	}

	void I2PService::SetConnectTimeout(uint64_t timeout)
	{
		m_ConnectTimeout = timeout;
	}

	void I2PService::SetCloseIdleTime (uint64_t idleTime)
	{
		if (idleTime > 0 && idleTime < I2P_SERVICE_MIN_CLOSE_IDLE_TIME) idleTime = I2P_SERVICE_MIN_CLOSE_IDLE_TIME;
		m_CloseIdleTime = idleTime;
	}

	void I2PService::UpdateLastActivityTime ()
	{
		if (m_CloseIdleTime)
		{
			m_LastActivityTime = i2p::util::GetMonotonicMilliseconds ();
			if (m_LocalDestination->IsIdling ())
				Resume ();
		}
	}

	void I2PService::Resume ()
	{
		if (m_CloseIdleTime)
		{
			if (m_NewDestOnResume)
			{
				auto ident = m_LocalDestination->GetPrivateKeys ().GetPublic ();
				if (ident)
				{
					m_LocalDestination->SetPrivateKeys (i2p::data::PrivateKeys::CreateRandomKeys (
						ident->GetSigningKeyType (), ident->GetCryptoKeyType (), true));
					i2p::client::context.ReplaceLocalDestinationHash (ident->GetIdentHash (), m_LocalDestination->GetIdentHash ());
				}
			}
			ScheduleIdleCheckTimer ();
		}
		m_LocalDestination->SetIsIdling (false);
	}

	void I2PService::AddReadyCallback(ReadyCallback cb)
	{
		uint64_t now = i2p::util::GetMonotonicSeconds ();
		uint64_t tm = (m_ConnectTimeout) ? now + m_ConnectTimeout : NEVER_TIMES_OUT;

		LogPrint(eLogDebug, "I2PService::AddReadyCallback() ", tm, " ", now);
		m_ReadyCallbacks.push_back({cb, tm});
		if (!m_ReadyTimerTriggered) TriggerReadyCheckTimer();
	}

	void I2PService::TriggerReadyCheckTimer()
	{
		m_ReadyTimer.expires_after(std::chrono::seconds (I2P_SERVICE_READINESS_CHECK_INTERVAL));
		m_ReadyTimer.async_wait(std::bind(&I2PService::HandleReadyCheckTimer, shared_from_this (), std::placeholders::_1));
		m_ReadyTimerTriggered = true;
	}

	void I2PService::HandleReadyCheckTimer(const boost::system::error_code &ec)
	{
		bool isReady = (!ec) ? m_LocalDestination->IsReady() : false;
		if(ec || isReady)
		{
			for(auto & itr : m_ReadyCallbacks)
				itr.first(ec);
			m_ReadyCallbacks.clear();
		}
		else if(!isReady)
		{
			// expire timed out requests
			uint64_t now = i2p::util::GetMonotonicSeconds ();
			auto itr = m_ReadyCallbacks.begin();
			while(itr != m_ReadyCallbacks.end())
			{
				if(itr->second != NEVER_TIMES_OUT && now >= itr->second)
				{
					itr->first(boost::asio::error::timed_out);
					itr = m_ReadyCallbacks.erase(itr);
				}
				else
					++itr;
			}
		}
		if(!ec && m_ReadyCallbacks.size())
			TriggerReadyCheckTimer();
		else
			m_ReadyTimerTriggered = false;
	}

	void I2PService::ScheduleIdleCheckTimer ()
	{
		if (!m_IdleCheckTimer || !m_CloseIdleTime) return;
		m_IdleCheckTimer->expires_after(std::chrono::milliseconds (m_CloseIdleTime/2));
		m_IdleCheckTimer->async_wait(std::bind(&I2PService::HandleIdleCheckTimer, shared_from_this (), std::placeholders::_1));
	}

	void I2PService::HandleIdleCheckTimer(const boost::system::error_code & ec)
	{
		if (ec != boost::asio::error::operation_aborted)
		{
			auto ts = i2p::util::GetMonotonicMilliseconds ();
			if (ts > m_LastActivityTime + m_CloseIdleTime)
				m_LocalDestination->SetIsIdling (true);
			else
				ScheduleIdleCheckTimer ();
		}
	}

	void I2PService::CreateStream (StreamRequestComplete streamRequestComplete, std::string_view dest, uint16_t port) {
		assert(streamRequestComplete);
		auto address = i2p::client::context.GetAddressBook ().GetAddress (dest);
		if (address)
			CreateStream(streamRequestComplete, address, port);
		else
		{
			LogPrint (eLogWarning, "I2PService: Remote destination not found: ", dest);
			streamRequestComplete (nullptr);
		}
	}

	void I2PService::CreateStream(StreamRequestComplete streamRequestComplete, std::shared_ptr<const Address> address, uint16_t port)
	{
		if(m_ConnectTimeout && !m_LocalDestination->IsReady())
		{
			AddReadyCallback([this, streamRequestComplete, address, port] (const boost::system::error_code & ec)
				{
					if(ec)
					{
						LogPrint(eLogWarning, "I2PService::CreateStream() ", ec.message());
						streamRequestComplete(nullptr);
					}
					else
					{
						if (address->IsIdentHash ())
							this->m_LocalDestination->CreateStream(streamRequestComplete, address->identHash, port);
						else
							this->m_LocalDestination->CreateStream (streamRequestComplete, address->blindedPublicKey, port);
					}
				});
		}
		else
		{
			if (address->IsIdentHash ())
				m_LocalDestination->CreateStream (streamRequestComplete, address->identHash, port);
			else
				m_LocalDestination->CreateStream (streamRequestComplete, address->blindedPublicKey, port);
		}
	}
}
}
