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

using System.Collections.Concurrent;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Core;

namespace Microsoft.AspNetCore.Mvc.Filters;

/// <summary>
/// Builds a middleware pipeline after receiving the pipeline from a pipeline provider
/// </summary>
internal sealed class MiddlewareFilterBuilder
{
    // 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the pipeline more than
    // once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple
    // threads but only one of the objects succeeds in creating a pipeline.
    private readonly ConcurrentDictionary<Type, Lazy<RequestDelegate>> _pipelinesCache
        = new ConcurrentDictionary<Type, Lazy<RequestDelegate>>();
    private readonly MiddlewareFilterConfigurationProvider _configurationProvider;

    public IApplicationBuilder? ApplicationBuilder { get; set; }

    public MiddlewareFilterBuilder(MiddlewareFilterConfigurationProvider configurationProvider)
    {
        _configurationProvider = configurationProvider;
    }

    public RequestDelegate GetPipeline(Type configurationType)
    {
        // Build the pipeline only once. This is similar to how middleware registered in Startup are constructed.

        var requestDelegate = _pipelinesCache.GetOrAdd(
            configurationType,
            key => new Lazy<RequestDelegate>(() => BuildPipeline(key)));

        return requestDelegate.Value;
    }

    private RequestDelegate BuildPipeline(Type middlewarePipelineProviderType)
    {
        if (ApplicationBuilder == null)
        {
            throw new InvalidOperationException(
                Resources.FormatMiddlewareFilterBuilder_NullApplicationBuilder(nameof(ApplicationBuilder)));
        }

        var nestedAppBuilder = ApplicationBuilder.New();

        // Get the 'Configure' method from the user provided type.
        var configureDelegate = MiddlewareFilterConfigurationProvider.CreateConfigureDelegate(middlewarePipelineProviderType);
        configureDelegate(nestedAppBuilder);

        // The middleware resource filter, after receiving the request executes the user configured middleware
        // pipeline. Since we want execution of the request to continue to later MVC layers (resource filters
        // or model binding), add a middleware at the end of the user provided pipeline which make sure to continue
        // this flow.
        // Example:
        // middleware filter -> user-middleware1 -> user-middleware2 -> end-middleware -> resource filters or model binding
        nestedAppBuilder.Run(async (httpContext) =>
        {
            var feature = httpContext.Features.GetRequiredFeature<IMiddlewareFilterFeature>();

            var resourceExecutionDelegate = feature.ResourceExecutionDelegate!;

            var resourceExecutedContext = await resourceExecutionDelegate();
            if (resourceExecutedContext.ExceptionHandled)
            {
                return;
            }

            // Ideally we want the experience of a middleware pipeline to behave the same as if it was registered
            // in Startup. In this scenario, an Exception thrown in a middleware later in the pipeline gets
            // propagated back to earlier middleware. So, check if a later resource filter threw an Exception and
            // propagate that back to the middleware pipeline.
            resourceExecutedContext.ExceptionDispatchInfo?.Throw();
            if (resourceExecutedContext.Exception != null)
            {
                // This line is rarely reachable because ResourceInvoker captures thrown Exceptions using
                // ExceptionDispatchInfo. That said, filters could set only resourceExecutedContext.Exception.
                throw resourceExecutedContext.Exception;
            }
        });

        return nestedAppBuilder.Build();
    }
}
