/*
* Copyright (c) 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
*/

#ifndef TORRENTS_H__
#define TORRENTS_H__

#include <inttypes.h>
#include <openssl/sha.h>
#include <boost/asio.hpp>
#include <boost/dynamic_bitset.hpp>
#include <memory>
#include <vector>
#include <array>
#include <list>
#include <string>
#include <string_view>
#include <random>
#include <tuple>
#include <optional>
#include <filesystem>
#include "util.h"
#include "Streaming.h"
#include "I2PService.h"

namespace i2p
{
namespace torrents
{
	constexpr size_t REQUEST_BLOCK_SIZE = 16384;
	constexpr uint16_t TORRENT_PORT = 6881; //  not used by required by protocol
	constexpr int MIN_TRACKER_REQUESTS_INTERVAL = 15000; // in milliseconds
	constexpr size_t PEER_CONNECTION_RECEIVE_BUFFER_SIZE = 65535;
	constexpr int PEER_CONNECTION_MAX_IDLE = 3600; // in seconds
	constexpr int PEER_KEEP_ALIVE_TIMEOUT = 120; // in seconds
	constexpr int PEER_KEEP_SEND_INTERVAL = 95; // in seconds
	constexpr size_t MAX_NUM_REQUESTS = 12;
	constexpr size_t MAX_NUM_PIECES = 6;
	constexpr int PIECE_INACTIVITY_TIMEOUT = 60; // in seconds
	constexpr int HANDSHAKE_RECEIVE_TIMEOUT = 20; // in seconds
	constexpr int BANDWIDTH_RATE_SAMPLING_INTERVAL = 20; // in milliseconds

	constexpr size_t HANDSHAKE_MSG_LENGTH = 68;
	constexpr size_t INTERESTED_MSG_LENGTH = 5;
	constexpr size_t NOTINTERESTED_MSG_LENGTH = 5;
	constexpr size_t CHOKE_MSG_LENGTH = 5;
	constexpr size_t UNCHOKE_MSG_LENGTH = 5;
	constexpr size_t REQUEST_MSG_PAYLOAD_LENGTH = 12;
	constexpr size_t REQUEST_MSG_LENGTH = REQUEST_MSG_PAYLOAD_LENGTH + 5;
	constexpr size_t HAVE_MSG_PAYLOAD_LENGTH = 4;

	enum MessageType
	{
		eMessageTypeChoke = 0,
		eMessageTypeUnchoke = 1,
		eMessageTypeInterested = 2,
		eMessageTypeNotInterested = 3,
		eMessageTypeHave = 4,
		eMessageTypeBitfield = 5,
		eMessageTypeRequest = 6,
		eMessageTypePiece = 7,
		eMessageTypeHaveAll = 14,
		eMessageTypeHaveNone = 15,
		eMessageTypeExtended = 20 // BEP10
	};

	struct PieceFileFragment // fragment to save to/load from file
	{
		std::filesystem::path fullFilePath;
		size_t fileOffset;
		size_t fragmentOffset; // from start of piece
		size_t fragmentSize;

		PieceFileFragment (const std::filesystem::path& fullFilePath1, size_t fileOffset1, size_t fragmentOffset1, size_t fragmentSize1):
			fullFilePath (fullFilePath1), fileOffset (fileOffset1), fragmentOffset (fragmentOffset1), fragmentSize (fragmentSize1) {};
		PieceFileFragment (PieceFileFragment&& ) = default;
		PieceFileFragment (const PieceFileFragment& ) = default;
	};

	class PeerConnection;
	class Piece final
	{
		enum class BlockStatus
		{
			Missing,
			Available,
			Requested
		};

		public:

			Piece (size_t size, const uint8_t * hash);
			Piece (Piece&& ) = default;
			~Piece ();

			bool IsComplete () const { return !m_Blocks; }
			void Complete () { m_Blocks = nullptr; }
			bool VerifyHash () const;
			void SetIsSending (bool isSending);
			uint64_t GetLastActivityTimestamp () const { return m_LastActivityTimestamp; }
			bool IsRequested () const { return m_IsRequested; }
			size_t GetNumPeers () const { return m_NumPeers; }
			void SetNumPeers (size_t numPeers) { m_NumPeers = numPeers; }

			void BlockReceived (const uint8_t * block, size_t len, size_t offset);
			void Dump (PieceFileFragment&& fragment);
			bool Load (PieceFileFragment&& fragment);
			const uint8_t * GetData () const { return m_Data; }
			size_t GetSize () const { return m_Size; }
			bool HasBlock (size_t offset) const;
			std::pair<size_t, size_t> GetNextBlockToRequest (); // return (offset, len) of next buffer, len = 0 if no next buffer
			void ClearAllRequests ();
			void InvalidateAllBlocks ();
			void Reset ();

		private:

			bool IsAvailable (int block) const;
			size_t GetNumBlocks (size_t len) const;

		private:

			size_t m_Size;
			uint8_t * m_Data, m_Hash[SHA_DIGEST_LENGTH];
			std::unique_ptr<std::vector<BlockStatus> > m_Blocks;
			bool m_IsSending, m_IsRequested;
			uint64_t m_LastActivityTimestamp; // monotonic seconds
			size_t m_NumPeers; // where this piece is available
	};

	enum TorrentStatus
	{
		eTorrentStatusStopped = 0,
		eTorrentStatusQueuedToVerifyLocalData = 1,
		eTorrentStatusVerifyingLocalData = 2,
		eTorrentStatusQueuedToDownload = 3,
		eTorrentStatusDownloading = 4,
		eTorrentStatusQueuedToSeed = 5,
		eTorrentStatusSeeding = 6
	};

	using RequestedBlock = std::tuple<uint32_t, uint32_t, uint32_t>; // (index, offset, len)
	class Torrent final
	{
		public:

			using InfoHash = std::array<uint8_t, 20>;
			using TrackerInfo = std::tuple<std::unordered_set<i2p::data::IdentHash>, int, uint64_t>; // (peers, interval, next tracker request time in monotonic milliseconds)

			Torrent (std::string_view buf);
			void ParseTrackerResponse (size_t trackerID, std::string_view buf);
			void HandleDatagramTrackerResponse (size_t trackerID, uint32_t interval, const uint8_t * hashes, size_t hashesLen);

			bool IsComplete () const { return m_IsComplete; }
			void SetComplete ();
			TorrentStatus GetStatus () const { return m_IsComplete ? eTorrentStatusSeeding : eTorrentStatusDownloading; };

			std::string_view GetAnnounce () const { return m_Announce; }
			std::string_view GetName () const { return m_Name; }
			bool IsValid () const { return !m_Name.empty () && m_PieceLength && (m_Length || !m_Files.empty ()); }
			const std::filesystem::path& GetFullPath () const { return m_FullPath; }
			void SetFullPath (const std::filesystem::path& fullPath) { m_FullPath = fullPath; }
			const std::list<std::pair<std::filesystem::path, size_t> >& GetFiles () const { return m_Files; }
			std::list<std::pair<std::filesystem::path, size_t> >& GetFiles () { return m_Files; }
			size_t GetLength () const { return m_Length; }
			size_t GetPieceLength () const { return m_PieceLength; }
			int GetInterval (size_t trackerID) const { return (trackerID < m_TrackersInfo.size ()) ? std::get<1>(m_TrackersInfo[trackerID]) : MIN_TRACKER_REQUESTS_INTERVAL; }
			const InfoHash& GetInfoHash () const { return m_InfoHash; }
			size_t GetLeft () const;
			std::string GetHexStringInfoHash () const; // in url format
			size_t GetNumPieces () const { return m_Pieces.size (); }
			Piece& GetPiece (int index) { return m_Pieces[index]; }
			std::pair<std::vector<uint8_t>, bool> CreateBitfield () const; // (bitfield, empty)
			bool ApplyBitfield (const std::vector<uint8_t>& bitfield); // return true if complete
			std::unordered_set<i2p::data::IdentHash>  GetPeers () const;
			RequestedBlock GetNextBlockToRequest (std::shared_ptr<PeerConnection> conn, bool skipRequested = true);
			std::vector<PieceFileFragment> GetPieceFileFragments (int index) const;
			std::vector<size_t> GetFilesCompleted () const; // completed size per file
			bool UpdateStatus (uint64_t ts); // return true if complete

			uint64_t GetNextTrackerRequestTime (size_t trackerID) const;
			void SetNextTrackerRequestTime (size_t trackerID, uint64_t ts);
			size_t GetUploaded () const { return m_Uploaded; }
			void AddUploaded (size_t add) { m_Uploaded += add; }
			size_t GetDownloaded () const { return m_Downloaded; }
			void AddDownloaded (size_t add) { m_Downloaded += add; }

			void SaveTorrentResumeFile (const std::filesystem::path& fullPath);

			void StartCountingPeers ();
			void ApplyPeerRemoteBitfield (const boost::dynamic_bitset<>& peerRemoteBitfield);
			bool HasIncompletePieces (const boost::dynamic_bitset<>& peerRemoteBitfield) const; // if remote bitfie;d has incomplete pieces

			void ResetStats () { m_DownloadRate = 0; m_UploadRate = 0;  m_NumDownloadingFromPeers = 0; m_NumUploadingToPeers = 0; }
			uint64_t GetDownloadRate () const { return m_DownloadRate; }
			void SetDownloadRate (uint64_t downloadRate) { m_DownloadRate = downloadRate; }
			uint64_t GetUploadRate () const { return m_UploadRate; }
			void SetUploadRate (uint64_t uploadRate) { m_UploadRate = uploadRate; }
			int GetNumDownloadingFromPeers () const { return m_NumDownloadingFromPeers; }
			void SetNumDownloadingFromPeers (int numDownloadingFromPeers) { m_NumDownloadingFromPeers = numDownloadingFromPeers; }
			int GetNumUploadingToPeers () const { return m_NumUploadingToPeers; }
			void SetNumUploadingToPeers (int numUploadingToPeers) { m_NumUploadingToPeers = numUploadingToPeers; }

		private:

			size_t ParsePieces (std::string_view buf);
			size_t ParseInfo (std::string_view buf);
			size_t ParsePeers (size_t trackerID, std::string_view buf);
			size_t ParseFiles (std::string_view buf);
			static bool IsSafeName (std::string_view name); // a name from a torrent file before it becomes a path

		private:

			std::string m_Name, m_Announce;
			std::filesystem::path m_FullPath;
			size_t m_Length, m_PieceLength;
			InfoHash m_InfoHash; // SHA1
			std::vector<Piece> m_Pieces;
			std::vector<TrackerInfo> m_TrackersInfo;
			bool m_IsComplete;
			std::list<std::pair<std::filesystem::path, size_t> > m_Files; // list of (path, length)
			size_t m_Uploaded, m_Downloaded;
			// stats
			uint64_t m_DownloadRate, m_UploadRate; // B/sec
			int m_NumDownloadingFromPeers, m_NumUploadingToPeers; // by us
	};

	class TorrentsTunnel;
	class PeerConnection: public i2p::client::I2PServiceHandler, public std::enable_shared_from_this<PeerConnection>
	{
		public:

			using PeerID = std::array<uint8_t, 20>;

			PeerConnection (std::shared_ptr<i2p::client::I2PService> owner,  std::shared_ptr<i2p::stream::Stream> stream); // incoming
			PeerConnection (std::shared_ptr<i2p::client::I2PService> owner,  std::shared_ptr<i2p::stream::Stream> stream,
				std::shared_ptr<Torrent> torrent); // outgoing
			~PeerConnection ();

			void Connect ();
			void Close ();
			void ReceiveHandshake ();
			void CheckKeepAlive (uint64_t ts);

			bool IsEstablished () const { return m_IsEstablished; };
			bool IsPieceAvailable (size_t ind) const;
			std::shared_ptr<i2p::stream::Stream> GetStream () const { return m_Stream; }
			std::shared_ptr<Torrent> GetTorrent () const { return m_Torrent; }
			int GetLastRequestedPieceIndex () const { return m_LastRequestedPieceIndex; }
			const boost::dynamic_bitset<>& GetRemoteBitfield () const  { return m_RemoteBitfield; }
			const PeerID& GetRemotePeerID () const { return m_RemotePeerID; }
			std::string_view GetRemoteName () const { return m_RemoteName; }

			// stats
			void ResetStats ();
			uint64_t GetDownloadRate () const { return m_DownloadRate; }
			uint64_t GetUploadRate () const { return m_UploadRate; }
			bool IsDownloading () const { return m_NumRequests > 0; }
			bool IsUploading () const { return m_NumPieces > 0 || (m_Stream && m_Stream->GetSendBufferSize () > 4); }
			bool IsInterested () const  { return m_IsInterested; }
			bool IsRemoteInterested () const  { return m_IsRemoteInterested; }
			bool IsChoked () const  { return m_IsChoked; }
			bool IsRemoteChoked () const  { return m_IsRemoteChoked; }
			size_t GetDownloaded () const { return m_Downloaded; }
			size_t GetUploaded () const { return m_Uploaded; }

		private:

			void Terminate ();
			void ScheduleHandshakeReceiveTimer ();
			std::shared_ptr<TorrentsTunnel> GetTorrentsTunnel () const;

			void WriteToStream (const uint8_t * buf, size_t len);
			void StreamReceive ();
			void HandleStreamReceive (const boost::system::error_code& ecode, size_t bytes_transferred);
			void HandleReceived ();
			size_t HandleNextMsg (size_t offset);

			size_t HandleHandshakeMsg ();
			void SendHandshakeMsg ();

			void HandleHaveMsg (const uint8_t * buf, size_t len);
			void SendHaveMsg (uint32_t index);
			void HandleBitfieldMsg (const uint8_t * buf, size_t len);
			void SendBitfieldMsg (const uint8_t * bitfield, size_t bitfieldLen);
			void HandleHaveAllMsg ();
			void HandleHaveNoneMsg ();
			void HandlePieceMsg (const uint8_t * buf, size_t len);
			void SendPieceMsg (uint32_t index, uint32_t offset, const uint8_t * data, size_t len);
			void HandleRequestMsg (const uint8_t * buf, size_t len);
			void SendRequestMsg (uint32_t index, uint32_t offset, uint32_t len);
			size_t FillRequestMsg (uint8_t * buf, uint32_t index, uint32_t offset, uint32_t len);
			void SendInterestedMsg ();
			void SendNotinterestedMsg ();
			void SendUnchokeMsg ();
			void SendChokeMsg ();
			void HandleChokeMsg ();
			void HandleExtendedMsg (const uint8_t * buf, size_t len);
			void SendExtendedMsg ();

			std::optional<RequestedBlock> GetNextBlockToRequest ();
			bool RequestNextBlocks ();
			bool SendRequestedBlock (const RequestedBlock& requestedBlock);

		private:

			std::shared_ptr<i2p::stream::Stream> m_Stream;
			uint8_t m_ReceiveBuffer[PEER_CONNECTION_RECEIVE_BUFFER_SIZE];
			size_t m_ReceiveBufferOffset, m_NextMsgLength;
			std::shared_ptr<Torrent> m_Torrent;
			PeerID m_RemotePeerID;
			std::string m_RemoteName; // from BEP10
			boost::dynamic_bitset<> m_RemoteBitfield;
			bool m_IsHandshakeSent, m_IsEstablished, m_IsChoked, m_IsRemoteChoked,
				m_IsInterested, m_IsRemoteInterested;
			uint64_t m_LastReceiveTime, m_LastSendTime; // monotonic seconds
			size_t m_NumRequests, m_NumPieces; // outgoing
			std::list<RequestedBlock> m_IncomingRequestsQueue;
			int m_LastRequestedPieceIndex;
			std::unique_ptr<boost::asio::steady_timer> m_HandshakeReceiveTimer;
			// stats
			uint64_t m_DownloadRate, m_UploadRate; // B/sec
			uint64_t m_LastBlockDownloadTimestamp, m_LastBlockUploadTimestamp; // monotonic milliseconds
			size_t m_ReceivedSinceLastTimestamp, m_SentSinceLastTimestamp; // bytes
			size_t m_Downloaded, m_Uploaded; // bytes
	};
}
}
#endif
