#define MS_CLASS "RTC::SCTP::Packet"
// #define MS_LOG_DEV_LEVEL 3

#include "RTC/SCTP/association/StateCookie.hpp"
#include "Logger.hpp"
#include "MediaSoupErrors.hpp"
#include <cstring> // std::memcpy(), std::memcmp()
#include <string_view>

namespace RTC
{
	namespace SCTP
	{
		/* Class methods. */

		StateCookie* StateCookie::Parse(const uint8_t* buffer, size_t bufferLength)
		{
			MS_TRACE();

			if (!StateCookie::IsMediasoupStateCookie(buffer, bufferLength))
			{
				MS_WARN_TAG(sctp, "not a StateCookie generated by mediasoup");

				return nullptr;
			}

			auto* stateCookie = new StateCookie(const_cast<uint8_t*>(buffer), bufferLength);

			return stateCookie;
		}

		StateCookie* StateCookie::Factory(
		  uint8_t* buffer,
		  size_t bufferLength,
		  uint32_t localVerificationTag,
		  uint32_t remoteVerificationTag,
		  uint32_t localInitialTsn,
		  uint32_t remoteInitialTsn,
		  uint32_t remoteAdvertisedReceiverWindowCredit,
		  uint64_t tieTag,
		  const NegotiatedCapabilities& negotiatedCapabilities,
		  uint64_t creationTimestampMs,
		  const uint8_t* macKey,
		  size_t macKeyLength)
		{
			MS_TRACE();

			// This may throw.
			StateCookie::Write(
			  buffer,
			  bufferLength,
			  localVerificationTag,
			  remoteVerificationTag,
			  localInitialTsn,
			  remoteInitialTsn,
			  remoteAdvertisedReceiverWindowCredit,
			  tieTag,
			  negotiatedCapabilities,
			  creationTimestampMs,
			  macKey,
			  macKeyLength);

			const size_t cookieLength = macKey != nullptr ? StateCookie::AuthenticatedStateCookieLength
			                                              : StateCookie::StateCookieLength;

			return new StateCookie(buffer, cookieLength);
		}

		void StateCookie::Write(
		  uint8_t* buffer,
		  size_t bufferLength,
		  uint32_t localVerificationTag,
		  uint32_t remoteVerificationTag,
		  uint32_t localInitialTsn,
		  uint32_t remoteInitialTsn,
		  uint32_t remoteAdvertisedReceiverWindowCredit,
		  uint64_t tieTag,
		  const NegotiatedCapabilities& negotiatedCapabilities,
		  uint64_t creationTimestampMs,
		  const uint8_t* macKey,
		  size_t macKeyLength)
		{
			MS_TRACE();

			const bool authenticate = macKey != nullptr;
			const size_t cookieLength =
			  authenticate ? StateCookie::AuthenticatedStateCookieLength : StateCookie::StateCookieLength;

			if (bufferLength < cookieLength)
			{
				MS_THROW_TYPE_ERROR("buffer too small");
			}

			Utils::Byte::Set8Bytes(buffer, 0, StateCookie::Magic1);
			Utils::Byte::Set4Bytes(buffer, 8, localVerificationTag);
			Utils::Byte::Set4Bytes(buffer, 12, remoteVerificationTag);
			Utils::Byte::Set4Bytes(buffer, 16, localInitialTsn);
			Utils::Byte::Set4Bytes(buffer, 20, remoteInitialTsn);
			Utils::Byte::Set4Bytes(buffer, 24, remoteAdvertisedReceiverWindowCredit);
			Utils::Byte::Set8Bytes(buffer, 28, tieTag);

			auto* negotiatedCapabilitiesField = reinterpret_cast<NegotiatedCapabilitiesField*>(
			  buffer + StateCookie::NegotiatedCapabilitiesOffset);

			negotiatedCapabilitiesField->reserved = 0;
			negotiatedCapabilitiesField->bitA     = negotiatedCapabilities.partialReliability;
			negotiatedCapabilitiesField->bitB     = negotiatedCapabilities.messageInterleaving;
			negotiatedCapabilitiesField->bitC     = negotiatedCapabilities.reConfig;
			negotiatedCapabilitiesField->bitD     = negotiatedCapabilities.zeroChecksum;
			negotiatedCapabilitiesField->magic2   = htons(StateCookie::Magic2);
			negotiatedCapabilitiesField->negotiatedMaxOutboundStreams =
			  htons(negotiatedCapabilities.negotiatedMaxOutboundStreams);
			negotiatedCapabilitiesField->negotiatedMaxInboundStreams =
			  htons(negotiatedCapabilities.negotiatedMaxInboundStreams);

			if (!authenticate)
			{
				return;
			}

			// Append the creation timestamp and the MAC computed over all preceding
			// bytes (including the timestamp).
			//
			// @see RFC 9260 section 5.1.3.
			Utils::Byte::Set8Bytes(buffer, StateCookie::TimestampOffset, creationTimestampMs);

			const uint8_t* mac = Utils::Crypto::GetHmacSha1(
			  reinterpret_cast<const char*>(macKey), macKeyLength, buffer, StateCookie::MacOffset);

			std::memcpy(buffer + StateCookie::MacOffset, mac, StateCookie::MacLength);
		}

		bool StateCookie::IsMediasoupStateCookie(const uint8_t* buffer, size_t bufferLength)
		{
			MS_TRACE();

			if (bufferLength != StateCookie::StateCookieLength && bufferLength != StateCookie::AuthenticatedStateCookieLength)
			{
				return false;
			}

			if (Utils::Byte::Get8Bytes(buffer, 0) != StateCookie::Magic1)
			{
				return false;
			}

			auto* negotiatedCapabilitiesField = reinterpret_cast<NegotiatedCapabilitiesField*>(
			  const_cast<uint8_t*>(buffer) + StateCookie::NegotiatedCapabilitiesOffset);

			if (ntohs(negotiatedCapabilitiesField->magic2) != StateCookie::Magic2)
			{
				return false;
			}

			return true;
		}

		bool StateCookie::VerifyMac(
		  const uint8_t* buffer, size_t bufferLength, const uint8_t* macKey, size_t macKeyLength)
		{
			MS_TRACE();

			// An authenticated cookie has a fixed length.
			if (bufferLength != StateCookie::AuthenticatedStateCookieLength)
			{
				return false;
			}

			// Recompute the MAC over all bytes preceding the MAC field.
			const uint8_t* expectedMac = Utils::Crypto::GetHmacSha1(
			  reinterpret_cast<const char*>(macKey), macKeyLength, buffer, StateCookie::MacOffset);

			return std::memcmp(buffer + StateCookie::MacOffset, expectedMac, StateCookie::MacLength) == 0;
		}

		Types::SctpImplementation StateCookie::DetermineSctpImplementation(
		  const uint8_t* buffer, size_t bufferLength)
		{
			MS_TRACE();

			if (bufferLength < StateCookie::Magic1Length)
			{
				return Types::SctpImplementation::UNKNOWN;
			}

			const std::string_view magic1(reinterpret_cast<const char*>(buffer), StateCookie::Magic1Length);

			if (magic1 == "msworker")
			{
				return Types::SctpImplementation::MEDIASOUP;
			}
			else if (magic1 == "dcSCTP00")
			{
				return Types::SctpImplementation::DCSCTP;
			}
			else if (magic1 == "KAME-BSD")
			{
				return Types::SctpImplementation::USRSCTP;
			}
			else
			{
				return Types::SctpImplementation::UNKNOWN;
			}
		}

		/* Instance methods. */

		StateCookie::StateCookie(uint8_t* buffer, size_t bufferLength)
		  : Serializable(buffer, bufferLength)
		{
			MS_TRACE();

			// The cookie length is determined by whether it carries a MAC. When the
			// cookie is cloned into a larger buffer, `CloneInto()` will set the
			// proper length afterwards.
			SetLength(
			  bufferLength == StateCookie::AuthenticatedStateCookieLength
			    ? StateCookie::AuthenticatedStateCookieLength
			    : StateCookie::StateCookieLength);
		}

		StateCookie::~StateCookie()
		{
			MS_TRACE();
		}

		void StateCookie::Dump(int indentation) const
		{
			MS_TRACE();

			auto negotiatedCapabilities = GetNegotiatedCapabilities();

			MS_DUMP_CLEAN(indentation, "<SCTP::StateCookie>");
			MS_DUMP_CLEAN(indentation, "  length: %zu (buffer length: %zu)", GetLength(), GetBufferLength());
			MS_DUMP_CLEAN(indentation, "  local verification tag: %" PRIu32, GetLocalVerificationTag());
			MS_DUMP_CLEAN(indentation, "  remote verification tag: %" PRIu32, GetRemoteVerificationTag());
			MS_DUMP_CLEAN(indentation, "  local initial tsn: %" PRIu32, GetLocalInitialTsn());
			MS_DUMP_CLEAN(indentation, "  remote initial tsn: %" PRIu32, GetRemoteInitialTsn());
			MS_DUMP_CLEAN(
			  indentation,
			  "  remote advertised receiver window credit: %" PRIu32,
			  GetRemoteAdvertisedReceiverWindowCredit());
			MS_DUMP_CLEAN(indentation, "  tie-tag: %" PRIu64, GetTieTag());
			MS_DUMP_CLEAN(indentation, "  authenticated: %s", IsAuthenticated() ? "yes" : "no");

			if (IsAuthenticated())
			{
				MS_DUMP_CLEAN(indentation, "  creation timestamp (ms): %" PRIu64, GetCreationTimestampMs());
			}

			negotiatedCapabilities.Dump(indentation + 1);
			MS_DUMP_CLEAN(indentation, "</SCTP::StateCookie>");
		}

		StateCookie* StateCookie::Clone(uint8_t* buffer, size_t bufferLength) const
		{
			MS_TRACE();

			auto* clonedStateCookie = new StateCookie(buffer, bufferLength);

			Serializable::CloneInto(clonedStateCookie);

			return clonedStateCookie;
		}

		NegotiatedCapabilities StateCookie::GetNegotiatedCapabilities() const
		{
			MS_TRACE();

			auto* negotiatedCapabilitiesField = GetNegotiatedCapabilitiesField();

			NegotiatedCapabilities negotiatedCapabilities;

			negotiatedCapabilities.negotiatedMaxOutboundStreams =
			  ntohs(negotiatedCapabilitiesField->negotiatedMaxOutboundStreams);
			negotiatedCapabilities.negotiatedMaxInboundStreams =
			  ntohs(negotiatedCapabilitiesField->negotiatedMaxInboundStreams);
			negotiatedCapabilities.partialReliability  = negotiatedCapabilitiesField->bitA;
			negotiatedCapabilities.messageInterleaving = negotiatedCapabilitiesField->bitB;
			negotiatedCapabilities.reConfig            = negotiatedCapabilitiesField->bitC;
			negotiatedCapabilities.zeroChecksum        = negotiatedCapabilitiesField->bitD;

			// NOTE: No need to std::move(). Copy elision (RVO) is used for free in GCC
			// and clang in C++17 or higher.
			return negotiatedCapabilities;
		}
	} // namespace SCTP
} // namespace RTC
