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

using System.Net.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.HeaderPropagation;

/// <summary>
/// A message handler for propagating headers collected by the <see cref="HeaderPropagationMiddleware"/> to a outgoing request.
/// </summary>
public class HeaderPropagationMessageHandler : DelegatingHandler
{
    private readonly HeaderPropagationValues _values;
    private readonly HeaderPropagationMessageHandlerOptions _options;

    /// <summary>
    /// Creates a new instance of the <see cref="HeaderPropagationMessageHandler"/>.
    /// </summary>
    /// <param name="options">The options that define which headers are propagated.</param>
    /// <param name="values">The values of the headers to be propagated populated by the
    /// <see cref="HeaderPropagationMiddleware"/>.</param>
    public HeaderPropagationMessageHandler(HeaderPropagationMessageHandlerOptions options, HeaderPropagationValues values)
    {
        _options = options ?? throw new ArgumentNullException(nameof(options));
        _values = values ?? throw new ArgumentNullException(nameof(values));
    }

    /// <summary>
    /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation, after adding
    /// the propagated headers.
    /// </summary>
    /// <remarks>
    /// If an header with the same name is already present in the request, even if empty, the corresponding
    /// propagated header will not be added.
    /// </remarks>
    /// <param name="request">The HTTP request message to send to the server.</param>
    /// <param name="cancellationToken">A cancellation token to cancel operation.</param>
    /// <returns>The task object representing the asynchronous operation.</returns>
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var captured = _values.Headers;
        if (captured == null)
        {
            var message =
                $"The {nameof(HeaderPropagationValues)}.{nameof(HeaderPropagationValues.Headers)} property has not been " +
                $"initialized. Register the header propagation middleware by adding 'app.{nameof(HeaderPropagationApplicationBuilderExtensions.UseHeaderPropagation)}()' " +
                $"in the 'Configure(...)' method. Header propagation can only be used within the context of an HTTP request.";
            throw new InvalidOperationException(message);
        }

        // Perf: We iterate _options.Headers instead of iterating _values.Headers because iterating an IDictionary
        // will allocate. Also avoiding foreach since we don't define a struct-enumerator.
        var entries = _options.Headers;
        for (var i = 0; i < entries.Count; i++)
        {
            var entry = entries[i];
            var hasContent = request.Content != null;

            if (!request.Headers.TryGetValues(entry.OutboundHeaderName, out var _) &&
                !(hasContent && request.Content!.Headers.TryGetValues(entry.OutboundHeaderName, out var _)))
            {
                if (captured.TryGetValue(entry.CapturedHeaderName, out var stringValues) &&
                    !StringValues.IsNullOrEmpty(stringValues))
                {
                    if (stringValues.Count == 1)
                    {
                        var value = stringValues.ToString();
                        if (!request.Headers.TryAddWithoutValidation(entry.OutboundHeaderName, value) && hasContent)
                        {
                            request.Content!.Headers.TryAddWithoutValidation(entry.OutboundHeaderName, value);
                        }
                    }
                    else
                    {
                        var values = stringValues.ToArray();
                        if (!request.Headers.TryAddWithoutValidation(entry.OutboundHeaderName, values) && hasContent)
                        {
                            request.Content!.Headers.TryAddWithoutValidation(entry.OutboundHeaderName, values);
                        }
                    }
                }
            }
        }

        return base.SendAsync(request, cancellationToken);
    }
}
