// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using Microsoft.AspNetCore.Server.HttpSys;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.HttpSys.Internal;

#pragma warning disable CA1852 // Seal internal types
internal unsafe class NativeRequestContext : IDisposable
#pragma warning restore CA1852 // Seal internal types
{
    private const int AlignmentPadding = 8;
    private const int DefaultBufferSize = 4096 - AlignmentPadding;
    private IntPtr _originalBufferAddress;
    private readonly bool _useLatin1;
    private HttpApiTypes.HTTP_REQUEST* _nativeRequest;
    private readonly IMemoryOwner<byte>? _backingBuffer;
    private MemoryHandle _memoryHandle;
    private readonly int _bufferAlignment;
    private readonly bool _permanentlyPinned;
    private IReadOnlyDictionary<int, ReadOnlyMemory<byte>>? _requestInfo;

    private bool _disposed;
    private bool _pinsReleased;

    [MemberNotNullWhen(false, nameof(_backingBuffer))]
    private bool PermanentlyPinned => _permanentlyPinned;

    // To be used by HttpSys
    internal NativeRequestContext(MemoryPool<byte> memoryPool, uint? bufferSize, ulong requestId, bool useLatin1)
    {
        // TODO:
        // Apparently the HttpReceiveHttpRequest memory alignment requirements for non - ARM processors
        // are different than for ARM processors. We have seen 4 - byte - aligned buffers allocated on
        // virtual x64/x86 machines which were accepted by HttpReceiveHttpRequest without errors. In
        // these cases the buffer alignment may cause reading values at invalid offset. Setting buffer
        // alignment to 0 for now.
        //
        // _bufferAlignment = (int)(requestAddress.ToInt64() & 0x07);
        _bufferAlignment = 0;

        var newSize = (int)(bufferSize ?? DefaultBufferSize) + AlignmentPadding;
        if (newSize <= memoryPool.MaxBufferSize)
        {
            _backingBuffer = memoryPool.Rent(newSize);
        }
        else
        {
            // No size limit
            _backingBuffer = MemoryPool<byte>.Shared.Rent(newSize);
        }
        _backingBuffer.Memory.Span.Clear();
        _memoryHandle = _backingBuffer.Memory.Pin();
        _nativeRequest = (HttpApiTypes.HTTP_REQUEST*)((long)_memoryHandle.Pointer + _bufferAlignment);

        RequestId = requestId;
        _useLatin1 = useLatin1;
    }

    // To be used by IIS Integration.
    internal NativeRequestContext(HttpApiTypes.HTTP_REQUEST* request, bool useLatin1)
    {
        _useLatin1 = useLatin1;
        _nativeRequest = request;
        _bufferAlignment = 0;
        _permanentlyPinned = true;
    }

    public IReadOnlyDictionary<int, ReadOnlyMemory<byte>> RequestInfo => _requestInfo ??= GetRequestInfo();

    public ReadOnlySpan<long> Timestamps
    {
        get
        {
            /*
                Below is the definition of the timing info structure we are accessing the memory for.
                ULONG is 32-bit and maps to int. ULONGLONG is 64-bit and maps to long.

                typedef struct _HTTP_REQUEST_TIMING_INFO
                {
                    ULONG RequestTimingCount;
                    ULONGLONG RequestTiming[HttpRequestTimingTypeMax];

                } HTTP_REQUEST_TIMING_INFO, *PHTTP_REQUEST_TIMING_INFO;
            */

            if (!RequestInfo.TryGetValue((int)HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeRequestTiming, out var timingInfo))
            {
                return ReadOnlySpan<long>.Empty;
            }

            var timingCount = MemoryMarshal.Read<int>(timingInfo.Span);

            // Note that even though RequestTimingCount is an int, the compiler enforces alignment of data in the struct which causes 4 bytes
            // of padding to be added after RequestTimingCount, so we need to skip 64-bits before we get to the start of the RequestTiming array
            return MemoryMarshal.CreateReadOnlySpan(
                ref Unsafe.As<byte, long>(ref MemoryMarshal.GetReference(timingInfo.Span.Slice(sizeof(long)))),
                timingCount);
        }
    }

    internal HttpApiTypes.HTTP_REQUEST* NativeRequest
    {
        get
        {
            Debug.Assert(_nativeRequest != null || _backingBuffer == null, "native request accessed after ReleasePins().");
            return _nativeRequest;
        }
    }

    internal HttpApiTypes.HTTP_REQUEST_V2* NativeRequestV2
    {
        get
        {
            Debug.Assert(_nativeRequest != null || _backingBuffer == null, "native request accessed after ReleasePins().");
            return (HttpApiTypes.HTTP_REQUEST_V2*)_nativeRequest;
        }
    }

    internal ulong RequestId
    {
        get { return NativeRequest->RequestId; }
        set { NativeRequest->RequestId = value; }
    }

    internal ulong ConnectionId => NativeRequest->ConnectionId;

    internal ulong RawConnectionId => NativeRequest->RawConnectionId;

    internal HttpApiTypes.HTTP_VERB VerbId => NativeRequest->Verb;

    internal ulong UrlContext => NativeRequest->UrlContext;

    internal ushort UnknownHeaderCount => NativeRequest->Headers.UnknownHeaderCount;

    internal SslStatus SslStatus
    {
        get
        {
            return NativeRequest->pSslInfo == null ? SslStatus.Insecure :
                NativeRequest->pSslInfo->SslClientCertNegotiated == 0 ? SslStatus.NoClientCert :
                SslStatus.ClientCert;
        }
    }

    internal bool IsHttp2 => NativeRequest->Flags.HasFlag(HttpApiTypes.HTTP_REQUEST_FLAGS.Http2);

    internal bool IsHttp3 => NativeRequest->Flags.HasFlag(HttpApiTypes.HTTP_REQUEST_FLAGS.Http3);

    // Assumes memory isn't pinned. Will fail if called by IIS.
    internal uint Size
    {
        get
        {
            Debug.Assert(_backingBuffer != null);
            return (uint)_backingBuffer.Memory.Length - AlignmentPadding;
        }
    }

    /// <summary>
    /// Shows whether <see cref="ReleasePins"/> was already invoked on this native request context
    /// </summary>
    internal bool PinsReleased => _pinsReleased;

    // ReleasePins() should be called exactly once.  It must be called before Dispose() is called, which means it must be called
    // before an object (Request) which closes the RequestContext on demand is returned to the application.
    internal void ReleasePins()
    {
        Debug.Assert(_nativeRequest != null, "RequestContextBase::ReleasePins()|ReleasePins() called twice.");
        _originalBufferAddress = (IntPtr)_nativeRequest;
        _memoryHandle.Dispose();
        _memoryHandle = default;
        _nativeRequest = null;
        _pinsReleased = true;
    }

    public bool TryGetTimestamp(HttpSysRequestTimingType timestampType, out long timestamp)
    {
        var index = (int)timestampType;
        var timestamps = Timestamps;
        if (index < timestamps.Length && timestamps[index] > 0)
        {
            timestamp = timestamps[index];
            return true;
        }

        timestamp = default;
        return false;
    }

    public bool TryGetElapsedTime(HttpSysRequestTimingType startingTimestampType, HttpSysRequestTimingType endingTimestampType, out TimeSpan elapsed)
    {
        if (TryGetTimestamp(startingTimestampType, out long startTimestamp) && TryGetTimestamp(endingTimestampType, out long endTimestamp))
        {
            elapsed = Stopwatch.GetElapsedTime(startTimestamp, endTimestamp);
            return true;
        }

        elapsed = default;
        return false;
    }

    public virtual void Dispose()
    {
        if (!_disposed)
        {
            _disposed = true;
            Debug.Assert(_nativeRequest == null, "RequestContextBase::Dispose()|Dispose() called before ReleasePins().");
            _memoryHandle.Dispose();
            _backingBuffer?.Dispose();
        }
    }

    // These methods require the HTTP_REQUEST to still be pinned in its original location.

    internal string? GetVerb()
    {
        var verb = NativeRequest->Verb;
        if (verb > HttpApiTypes.HTTP_VERB.HttpVerbUnknown && verb < HttpApiTypes.HTTP_VERB.HttpVerbMaximum)
        {
            return HttpApiTypes.HttpVerbs[(int)verb];
        }
        else if (verb == HttpApiTypes.HTTP_VERB.HttpVerbUnknown && NativeRequest->pUnknownVerb != null)
        {
            // Never use Latin1 for the VERB
            return HeaderEncoding.GetString(NativeRequest->pUnknownVerb, NativeRequest->UnknownVerbLength, useLatin1: false);
        }

        return null;
    }

    internal string? GetRawUrl()
    {
        if (NativeRequest->pRawUrl != null && NativeRequest->RawUrlLength > 0)
        {
            return Marshal.PtrToStringAnsi((IntPtr)NativeRequest->pRawUrl, NativeRequest->RawUrlLength);
        }
        return null;
    }

    internal Span<byte> GetRawUrlInBytes()
    {
        if (NativeRequest->pRawUrl != null && NativeRequest->RawUrlLength > 0)
        {
            return new Span<byte>(NativeRequest->pRawUrl, NativeRequest->RawUrlLength);
        }

        return default;
    }

    internal CookedUrl GetCookedUrl()
    {
        return new CookedUrl(NativeRequest->CookedUrl);
    }

    internal Version GetVersion()
    {
        if (IsHttp3)
        {
            return HttpVersion.Version30;
        }
        if (IsHttp2)
        {
            return HttpVersion.Version20;
        }
        var major = NativeRequest->Version.MajorVersion;
        var minor = NativeRequest->Version.MinorVersion;
        if (major == 1 && minor == 1)
        {
            return HttpVersion.Version11;
        }
        else if (major == 1 && minor == 0)
        {
            return HttpVersion.Version10;
        }
        return new Version(major, minor);
    }

    internal bool CheckAuthenticated()
    {
        var requestInfo = NativeRequestV2->pRequestInfo;
        var infoCount = NativeRequestV2->RequestInfoCount;

        for (int i = 0; i < infoCount; i++)
        {
            var info = &requestInfo[i];
            if (info != null
                && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth)
            {
                var authInfo = (HttpApiTypes.HTTP_REQUEST_AUTH_INFO*)info->pInfo;
                if (authInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess)
                {
                    return true;
                }
            }
        }
        return false;
    }

    internal WindowsPrincipal GetUser()
    {
        var requestInfo = NativeRequestV2->pRequestInfo;
        var infoCount = NativeRequestV2->RequestInfoCount;

        for (int i = 0; i < infoCount; i++)
        {
            var info = &requestInfo[i];
            if (info != null
                && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth)
            {
                var authInfo = (HttpApiTypes.HTTP_REQUEST_AUTH_INFO*)info->pInfo;
                if (authInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess)
                {
                    // Duplicates AccessToken
                    var identity = new WindowsIdentity(authInfo->AccessToken, GetAuthTypeFromRequest(authInfo->AuthType));

                    // Close the original
                    UnsafeNclNativeMethods.SafeNetHandles.CloseHandle(authInfo->AccessToken);

                    return new WindowsPrincipal(identity);
                }
            }
        }

        return new WindowsPrincipal(WindowsIdentity.GetAnonymous()); // Anonymous / !IsAuthenticated
    }

    internal HttpApiTypes.HTTP_SSL_PROTOCOL_INFO GetTlsHandshake()
    {
        var requestInfo = NativeRequestV2->pRequestInfo;
        var infoCount = NativeRequestV2->RequestInfoCount;

        for (int i = 0; i < infoCount; i++)
        {
            var info = &requestInfo[i];
            if (info != null
                && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeSslProtocol)
            {
                var authInfo = *((HttpApiTypes.HTTP_SSL_PROTOCOL_INFO*)info->pInfo);
                SetSslProtocol(&authInfo);
                return authInfo;
            }
        }

        return default;
    }

    private static void SetSslProtocol(HttpApiTypes.HTTP_SSL_PROTOCOL_INFO* protocolInfo)
    {
        var protocol = protocolInfo->Protocol;
        // The OS considers client and server TLS as different enum values. SslProtocols choose to combine those for some reason.
        // We need to fill in the client bits so the enum shows the expected protocol.
        // https://learn.microsoft.com/windows/desktop/api/schannel/ns-schannel-_secpkgcontext_connectioninfo
        // Compare to https://referencesource.microsoft.com/#System/net/System/Net/SecureProtocols/_SslState.cs,8905d1bf17729de3
#pragma warning disable CS0618 // Type or member is obsolete
        if ((protocol & SslProtocols.Ssl2) != 0)
        {
            protocol |= SslProtocols.Ssl2;
        }
        if ((protocol & SslProtocols.Ssl3) != 0)
        {
            protocol |= SslProtocols.Ssl3;
        }
#pragma warning restore CS0618 // Type or Prmember is obsolete
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
        if ((protocol & SslProtocols.Tls) != 0)
        {
            protocol |= SslProtocols.Tls;
        }
        if ((protocol & SslProtocols.Tls11) != 0)
        {
            protocol |= SslProtocols.Tls11;
        }
#pragma warning restore SYSLIB0039
        if ((protocol & SslProtocols.Tls12) != 0)
        {
            protocol |= SslProtocols.Tls12;
        }
        if ((protocol & SslProtocols.Tls13) != 0)
        {
            protocol |= SslProtocols.Tls13;
        }

        protocolInfo->Protocol = protocol;
    }

    private static string GetAuthTypeFromRequest(HttpApiTypes.HTTP_REQUEST_AUTH_TYPE input)
    {
        switch (input)
        {
            case HttpApiTypes.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeBasic:
                return "Basic";
            case HttpApiTypes.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeNTLM:
                return "NTLM";
            // case HttpApi.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeDigest:
            //  return "Digest";
            case HttpApiTypes.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeNegotiate:
                return "Negotiate";
            case HttpApiTypes.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeKerberos:
                return "Kerberos";
            default:
                throw new NotImplementedException(input.ToString());
        }
    }

    internal bool HasKnownHeader(HttpSysRequestHeader header)
    {
        if (PermanentlyPinned)
        {
            return HasKnowHeaderHelper(header, 0, _nativeRequest);
        }
        else
        {
            fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
            {
                var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
                long fixup = pMemoryBlob - (byte*)_originalBufferAddress;
                return HasKnowHeaderHelper(header, fixup, request);
            }
        }
    }

    private bool HasKnowHeaderHelper(HttpSysRequestHeader header, long fixup, HttpApiTypes.HTTP_REQUEST* request)
    {
        int headerIndex = (int)header;        

        HttpApiTypes.HTTP_KNOWN_HEADER* pKnownHeader = (&request->Headers.KnownHeaders) + headerIndex;
        // For known headers, when header value is empty, RawValueLength will be 0 and
        // pRawValue will point to empty string ("\0")
        if (pKnownHeader->RawValueLength > 0)
        {
            return true;
        }

        return false;
    }

    // These methods are for accessing the request structure after it has been unpinned. They need to adjust addresses
    // in case GC has moved the original object.

    internal string? GetKnownHeader(HttpSysRequestHeader header)
    {
        if (PermanentlyPinned)
        {
            return GetKnowHeaderHelper(header, 0, _nativeRequest);
        }
        else
        {
            fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
            {
                var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
                long fixup = pMemoryBlob - (byte*)_originalBufferAddress;
                return GetKnowHeaderHelper(header, fixup, request);
            }
        }
    }  

    private string? GetKnowHeaderHelper(HttpSysRequestHeader header, long fixup, HttpApiTypes.HTTP_REQUEST* request)
    {
        int headerIndex = (int)header;
        string? value = null;

        HttpApiTypes.HTTP_KNOWN_HEADER* pKnownHeader = (&request->Headers.KnownHeaders) + headerIndex;
        // For known headers, when header value is empty, RawValueLength will be 0 and
        // pRawValue will point to empty string ("\0")
        if (pKnownHeader->RawValueLength > 0)
        {
            value = HeaderEncoding.GetString(pKnownHeader->pRawValue + fixup, pKnownHeader->RawValueLength, _useLatin1);
        }

        return value;
    }

    internal void GetUnknownKeys(Span<string> destination)
    {
        if (PermanentlyPinned)
        {
            PopulateUnknownKeys(_nativeRequest, 0, destination);
        }
        else
        {
            fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
            {
                var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
                long fixup = pMemoryBlob - (byte*)_originalBufferAddress;
                PopulateUnknownKeys(request, fixup, destination);
            }
        }
    }

    private void PopulateUnknownKeys(HttpApiTypes.HTTP_REQUEST* request, long fixup, Span<string> destination)
    {
        if (request->Headers.UnknownHeaderCount == 0)
        {
            return;
        }
        var pUnknownHeader = (HttpApiTypes.HTTP_UNKNOWN_HEADER*)(fixup + (byte*)request->Headers.pUnknownHeaders);
        for (int index = 0; index < request->Headers.UnknownHeaderCount; index++)
        {
            if (pUnknownHeader->pName != null && pUnknownHeader->NameLength > 0)
            {
                var headerName = HeaderEncoding.GetString(pUnknownHeader->pName + fixup, pUnknownHeader->NameLength, _useLatin1);
                destination[index] = headerName;
            }
            pUnknownHeader++;
        }
    }

    internal int CountUnknownHeaders()
    {
        if (PermanentlyPinned)
        {
            return CountUnknownHeaders(_nativeRequest, 0);
        }
        else
        {
            fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
            {
                var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
                long fixup = pMemoryBlob - (byte*)_originalBufferAddress;
                return CountUnknownHeaders(request, fixup);
            }
        }
    }

    private int CountUnknownHeaders(HttpApiTypes.HTTP_REQUEST* request, long fixup)
    {
        if (request->Headers.UnknownHeaderCount == 0)
        {
            return 0;
        }
        int count = 0;
        var pUnknownHeader = (HttpApiTypes.HTTP_UNKNOWN_HEADER*)(fixup + (byte*)request->Headers.pUnknownHeaders);
        for (int index = 0; index < request->Headers.UnknownHeaderCount; index++)
        {
            // For unknown headers, when header value is empty, RawValueLength will be 0 and
            // pRawValue will be null.
            if (pUnknownHeader->pName != null && pUnknownHeader->NameLength > 0)
            {
                count++;
            }
            pUnknownHeader++;
        }
        return count;
    }

    internal void GetUnknownHeaders(IDictionary<string, StringValues> unknownHeaders)
    {
        if (PermanentlyPinned)
        {
            GetUnknownHeadersHelper(unknownHeaders, 0, _nativeRequest);
        }
        else
        {
            // Return value.
            fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
            {
                var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
                long fixup = pMemoryBlob - (byte*)_originalBufferAddress;
                GetUnknownHeadersHelper(unknownHeaders, fixup, request);
            }
        }
    }

    private void GetUnknownHeadersHelper(IDictionary<string, StringValues> unknownHeaders, long fixup, HttpApiTypes.HTTP_REQUEST* request)
    {
        int index;

        // unknown headers
        if (request->Headers.UnknownHeaderCount != 0)
        {
            var pUnknownHeader = (HttpApiTypes.HTTP_UNKNOWN_HEADER*)(fixup + (byte*)request->Headers.pUnknownHeaders);
            for (index = 0; index < request->Headers.UnknownHeaderCount; index++)
            {
                // For unknown headers, when header value is empty, RawValueLength will be 0 and
                // pRawValue will be null.
                if (pUnknownHeader->pName != null && pUnknownHeader->NameLength > 0)
                {
                    var headerName = HeaderEncoding.GetString(pUnknownHeader->pName + fixup, pUnknownHeader->NameLength, _useLatin1);
                    string headerValue;
                    if (pUnknownHeader->pRawValue != null && pUnknownHeader->RawValueLength > 0)
                    {
                        headerValue = HeaderEncoding.GetString(pUnknownHeader->pRawValue + fixup, pUnknownHeader->RawValueLength, _useLatin1);
                    }
                    else
                    {
                        headerValue = string.Empty;
                    }
                    // Note that Http.Sys currently collapses all headers of the same name to a single coma separated string,
                    // so we can just call Set.
                    unknownHeaders[headerName] = headerValue;
                }
                pUnknownHeader++;
            }
        }
    }

    internal SocketAddress? GetRemoteEndPoint()
    {
        return GetEndPoint(localEndpoint: false);
    }

    internal SocketAddress? GetLocalEndPoint()
    {
        return GetEndPoint(localEndpoint: true);
    }

    private SocketAddress? GetEndPoint(bool localEndpoint)
    {
        if (PermanentlyPinned)
        {
            return GetEndPointHelper(localEndpoint, _nativeRequest, (byte*)0);
        }
        else
        {
            fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
            {
                var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
                return GetEndPointHelper(localEndpoint, request, pMemoryBlob);
            }
        }
    }

    private SocketAddress? GetEndPointHelper(bool localEndpoint, HttpApiTypes.HTTP_REQUEST* request, byte* pMemoryBlob)
    {
        var source = localEndpoint ? (byte*)request->Address.pLocalAddress : (byte*)request->Address.pRemoteAddress;

        if (source == null)
        {
            return null;
        }
        var address = (IntPtr)(pMemoryBlob + _bufferAlignment - (byte*)_originalBufferAddress + source);
        return CopyOutAddress(address);
    }

    private static SocketAddress? CopyOutAddress(IntPtr address)
    {
        ushort addressFamily = *((ushort*)address);
        if (addressFamily == (ushort)AddressFamily.InterNetwork)
        {
            var v4address = new SocketAddress(AddressFamily.InterNetwork, SocketAddress.IPv4AddressSize);
            fixed (byte* pBuffer = v4address.Buffer)
            {
                for (int index = 2; index < SocketAddress.IPv4AddressSize; index++)
                {
                    pBuffer[index] = ((byte*)address)[index];
                }
            }
            return v4address;
        }
        if (addressFamily == (ushort)AddressFamily.InterNetworkV6)
        {
            var v6address = new SocketAddress(AddressFamily.InterNetworkV6, SocketAddress.IPv6AddressSize);
            fixed (byte* pBuffer = v6address.Buffer)
            {
                for (int index = 2; index < SocketAddress.IPv6AddressSize; index++)
                {
                    pBuffer[index] = ((byte*)address)[index];
                }
            }
            return v6address;
        }

        return null;
    }

    internal uint GetChunks(ref int dataChunkIndex, ref uint dataChunkOffset, byte[] buffer, int offset, int size)
    {
        // Return value.
        if (PermanentlyPinned)
        {
            return GetChunksHelper(ref dataChunkIndex, ref dataChunkOffset, buffer, offset, size, 0, _nativeRequest);
        }
        else
        {
            fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
            {
                var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment);
                long fixup = pMemoryBlob - (byte*)_originalBufferAddress;
                return GetChunksHelper(ref dataChunkIndex, ref dataChunkOffset, buffer, offset, size, fixup, request);
            }
        }
    }

    private uint GetChunksHelper(ref int dataChunkIndex, ref uint dataChunkOffset, byte[] buffer, int offset, int size, long fixup, HttpApiTypes.HTTP_REQUEST* request)
    {
        uint dataRead = 0;

        if (request->EntityChunkCount > 0 && dataChunkIndex < request->EntityChunkCount && dataChunkIndex != -1)
        {
            var pDataChunk = (HttpApiTypes.HTTP_DATA_CHUNK*)(fixup + (byte*)&request->pEntityChunks[dataChunkIndex]);

            fixed (byte* pReadBuffer = buffer)
            {
                byte* pTo = &pReadBuffer[offset];

                while (dataChunkIndex < request->EntityChunkCount && dataRead < size)
                {
                    if (dataChunkOffset >= pDataChunk->fromMemory.BufferLength)
                    {
                        dataChunkOffset = 0;
                        dataChunkIndex++;
                        pDataChunk++;
                    }
                    else
                    {
                        byte* pFrom = (byte*)pDataChunk->fromMemory.pBuffer + dataChunkOffset + fixup;

                        uint bytesToRead = pDataChunk->fromMemory.BufferLength - (uint)dataChunkOffset;
                        if (bytesToRead > (uint)size)
                        {
                            bytesToRead = (uint)size;
                        }
                        for (uint i = 0; i < bytesToRead; i++)
                        {
                            *(pTo++) = *(pFrom++);
                        }
                        dataRead += bytesToRead;
                        dataChunkOffset += bytesToRead;
                    }
                }
            }
        }
        // we're finished.
        if (dataChunkIndex == request->EntityChunkCount)
        {
            dataChunkIndex = -1;
        }
        return dataRead;
    }

    internal IReadOnlyDictionary<int, ReadOnlyMemory<byte>> GetRequestInfo()
    {
        if (PermanentlyPinned)
        {
            return GetRequestInfo((IntPtr)_nativeRequest, (HttpApiTypes.HTTP_REQUEST_V2*)_nativeRequest);
        }
        else
        {
            fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
            {
                var request = (HttpApiTypes.HTTP_REQUEST_V2*)(pMemoryBlob + _bufferAlignment);
                return GetRequestInfo(_originalBufferAddress, request);
            }
        }
    }

    private IReadOnlyDictionary<int, ReadOnlyMemory<byte>> GetRequestInfo(IntPtr baseAddress, HttpApiTypes.HTTP_REQUEST_V2* nativeRequest)
    {
        var count = nativeRequest->RequestInfoCount;
        if (count == 0)
        {
            return ImmutableDictionary<int, ReadOnlyMemory<byte>>.Empty;
        }

        var info = new Dictionary<int, ReadOnlyMemory<byte>>(count);

        long fixup = (byte*)nativeRequest - (byte*)baseAddress;
        var pRequestInfo = (HttpApiTypes.HTTP_REQUEST_INFO*)((byte*)nativeRequest->pRequestInfo + fixup);

        for (var i = 0; i < count; i++)
        {
            var requestInfo = pRequestInfo[i];

            var memory = PermanentlyPinned
                ? new PointerMemoryManager<byte>((byte*)requestInfo.pInfo, (int)requestInfo.InfoLength).Memory
                : _backingBuffer.Memory.Slice((int)((long)requestInfo.pInfo - (long)baseAddress), (int)requestInfo.InfoLength);

            info.Add((int)requestInfo.InfoType, memory);
        }

        return new ReadOnlyDictionary<int, ReadOnlyMemory<byte>>(info);
    }

    internal X509Certificate2? GetClientCertificate()
    {
        if (PermanentlyPinned)
        {
            return GetClientCertificate((IntPtr)_nativeRequest, (HttpApiTypes.HTTP_REQUEST_V2*)_nativeRequest);
        }
        else
        {
            fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span)
            {
                var request = (HttpApiTypes.HTTP_REQUEST_V2*)(pMemoryBlob + _bufferAlignment);
                return GetClientCertificate(_originalBufferAddress, request);
            }
        }
    }

    // Throws CryptographicException
    private X509Certificate2? GetClientCertificate(IntPtr baseAddress, HttpApiTypes.HTTP_REQUEST_V2* nativeRequest)
    {
        var request = nativeRequest->Request;
        long fixup = (byte*)nativeRequest - (byte*)baseAddress;
        if (request.pSslInfo == null)
        {
            return null;
        }

        var sslInfo = (HttpApiTypes.HTTP_SSL_INFO*)((byte*)request.pSslInfo + fixup);
        if (sslInfo->SslClientCertNegotiated == 0 || sslInfo->pClientCertInfo == null)
        {
            return null;
        }

        var clientCertInfo = (HttpApiTypes.HTTP_SSL_CLIENT_CERT_INFO*)((byte*)sslInfo->pClientCertInfo + fixup);
        if (clientCertInfo->pCertEncoded == null)
        {
            return null;
        }

        var clientCert = clientCertInfo->pCertEncoded + fixup;
        byte[] certEncoded = new byte[clientCertInfo->CertEncodedSize];
        Marshal.Copy((IntPtr)clientCert, certEncoded, 0, certEncoded.Length);
        return new X509Certificate2(certEncoded);
    }

    // Copied from https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Memory/PointerMemoryManager.cs
    private sealed unsafe class PointerMemoryManager<T> : MemoryManager<T> where T : struct
    {
        private readonly void* _pointer;
        private readonly int _length;

        internal PointerMemoryManager(void* pointer, int length)
        {
            _pointer = pointer;
            _length = length;
        }

        protected override void Dispose(bool disposing)
        {
        }

        public override Span<T> GetSpan()
        {
            return new Span<T>(_pointer, _length);
        }

        public override MemoryHandle Pin(int elementIndex = 0)
        {
            throw new NotSupportedException();
        }

        public override void Unpin()
        {
        }
    }
}
