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

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Builder;

/// <summary>
/// Extension methods for enabling <see cref="StatusCodePagesMiddleware"/>.
/// </summary>
public static class StatusCodePagesExtensions
{
    /// <summary>
    /// Adds a StatusCodePages middleware with the given options that checks for responses with status codes
    /// between 400 and 599 that do not have a body.
    /// </summary>
    /// <param name="app"></param>
    /// <param name="options"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, StatusCodePagesOptions options)
    {
        ArgumentNullException.ThrowIfNull(app);
        ArgumentNullException.ThrowIfNull(options);

        return app.UseMiddleware<StatusCodePagesMiddleware>(Options.Create(options));
    }

    /// <summary>
    /// Adds a StatusCodePages middleware with a default response handler that checks for responses with status codes
    /// between 400 and 599 that do not have a body.
    /// </summary>
    /// <param name="app"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app)
    {
        ArgumentNullException.ThrowIfNull(app);

        return app.UseMiddleware<StatusCodePagesMiddleware>();
    }

    /// <summary>
    /// Adds a StatusCodePages middleware with the specified handler that checks for responses with status codes
    /// between 400 and 599 that do not have a body.
    /// </summary>
    /// <param name="app"></param>
    /// <param name="handler"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, Func<StatusCodeContext, Task> handler)
    {
        ArgumentNullException.ThrowIfNull(app);
        ArgumentNullException.ThrowIfNull(handler);

        return app.UseStatusCodePages(new StatusCodePagesOptions
        {
            HandleAsync = handler
        });
    }

    /// <summary>
    /// Adds a StatusCodePages middleware with the specified response body to send. This may include a '{0}' placeholder for the status code.
    /// The middleware checks for responses with status codes between 400 and 599 that do not have a body.
    /// </summary>
    /// <param name="app"></param>
    /// <param name="contentType"></param>
    /// <param name="bodyFormat"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, string contentType, string bodyFormat)
    {
        ArgumentNullException.ThrowIfNull(app);

        return app.UseStatusCodePages(context =>
        {
            var body = string.Format(CultureInfo.InvariantCulture, bodyFormat, context.HttpContext.Response.StatusCode);
            context.HttpContext.Response.ContentType = contentType;
            return context.HttpContext.Response.WriteAsync(body);
        });
    }

    /// <summary>
    /// Adds a StatusCodePages middleware to the pipeline. Specifies that responses should be handled by redirecting
    /// with the given location URL template. This may include a '{0}' placeholder for the status code. URLs starting
    /// with '~' will have PathBase prepended, where any other URL will be used as is.
    /// </summary>
    /// <param name="app"></param>
    /// <param name="locationFormat"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseStatusCodePagesWithRedirects(this IApplicationBuilder app, string locationFormat)
    {
        ArgumentNullException.ThrowIfNull(app);

        if (locationFormat.StartsWith('~'))
        {
            locationFormat = locationFormat.Substring(1);
            return app.UseStatusCodePages(context =>
            {
                var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode);
                context.HttpContext.Response.Redirect(context.HttpContext.Request.PathBase + location);
                return Task.CompletedTask;
            });
        }
        else
        {
            return app.UseStatusCodePages(context =>
            {
                var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode);
                context.HttpContext.Response.Redirect(location);
                return Task.CompletedTask;
            });
        }
    }

    /// <summary>
    /// Adds a StatusCodePages middleware to the pipeline with the specified alternate middleware pipeline to execute
    /// to generate the response body.
    /// </summary>
    /// <param name="app"></param>
    /// <param name="configuration"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, Action<IApplicationBuilder> configuration)
    {
        ArgumentNullException.ThrowIfNull(app);

        var builder = app.New();
        configuration(builder);
        var tangent = builder.Build();
        return app.UseStatusCodePages(context => tangent(context.HttpContext));
    }

    /// <summary>
    /// Adds a StatusCodePages middleware to the pipeline. Specifies that the response body should be generated by
    /// re-executing the request pipeline using an alternate path. This path may contain a '{0}' placeholder of the status code.
    /// </summary>
    /// <param name="app"></param>
    /// <param name="pathFormat"></param>
    /// <param name="queryFormat"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseStatusCodePagesWithReExecute(
        this IApplicationBuilder app,
        string pathFormat,
        string queryFormat)
    {
        ArgumentNullException.ThrowIfNull(app);

        // Only use this path if there's a global router (in the 'WebApplication' case).
        if (app.Properties.TryGetValue(RerouteHelper.GlobalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null)
        {
            return app.Use(next =>
            {
                var newNext = RerouteHelper.Reroute(app, routeBuilder, next);
                return new StatusCodePagesMiddleware(next,
                    Options.Create(new StatusCodePagesOptions() { HandleAsync = CreateHandler(pathFormat, queryFormat, newNext) })).Invoke;
            });
        }

        return app.UseStatusCodePages(CreateHandler(pathFormat, queryFormat));
    }

    /// <summary>
    /// Adds a StatusCodePages middleware to the pipeline. Specifies that the response body should be generated by
    /// re-executing the request pipeline using an alternate path. This path may contain a '{0}' placeholder of the status code.
    /// </summary>
    /// <param name="app"></param>
    /// <param name="pathFormat"></param>
    /// <param name="queryFormat"></param>
    /// <param name="createScopeForStatusCodePages">Whether or not to create a new <see cref="IServiceProvider"/> scope.</param>
    /// <returns></returns>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public static IApplicationBuilder UseStatusCodePagesWithReExecute(
        this IApplicationBuilder app,
        string pathFormat,
        string? queryFormat = null,
        bool createScopeForStatusCodePages = false)
    {
        ArgumentNullException.ThrowIfNull(app);

        // Only use this path if there's a global router (in the 'WebApplication' case).
        if (app.Properties.TryGetValue(RerouteHelper.GlobalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null)
        {
            return app.Use(next =>
            {
                var newNext = RerouteHelper.Reroute(app, routeBuilder, next);
                return new StatusCodePagesMiddleware(next,
                    Options.Create(new StatusCodePagesOptions()
                    {
                        HandleAsync = CreateHandler(pathFormat, queryFormat, newNext),
                        CreateScopeForStatusCodePages = createScopeForStatusCodePages,
                        PathFormat = pathFormat
                    })).Invoke;
            });
        }

        var options = new StatusCodePagesOptions
        {
            HandleAsync = CreateHandler(pathFormat, queryFormat),
            CreateScopeForStatusCodePages = createScopeForStatusCodePages,
            PathFormat = pathFormat
        };
        var wrappedOptions = new OptionsWrapper<StatusCodePagesOptions>(options);
        return app.UseMiddleware<StatusCodePagesMiddleware>(wrappedOptions);
    }

    private static Func<StatusCodeContext, Task> CreateHandler(string pathFormat, string? queryFormat, RequestDelegate? next = null)
    {
        var handler = async (StatusCodeContext context) =>
        {
            var originalStatusCode = context.HttpContext.Response.StatusCode;

            var newPath = new PathString(
                string.Format(CultureInfo.InvariantCulture, pathFormat, originalStatusCode));
            var formatedQueryString = queryFormat == null ? null :
                string.Format(CultureInfo.InvariantCulture, queryFormat, originalStatusCode);
            var newQueryString = queryFormat == null ? QueryString.Empty : new QueryString(formatedQueryString);

            var originalPath = context.HttpContext.Request.Path;
            var originalQueryString = context.HttpContext.Request.QueryString;

            var routeValuesFeature = context.HttpContext.Features.Get<IRouteValuesFeature>();
            var oldScope = context.Options.CreateScopeForStatusCodePages ? context.HttpContext.RequestServices : null;
            await using AsyncServiceScope? scope = context.Options.CreateScopeForStatusCodePages
                ? context.HttpContext.RequestServices.GetRequiredService<IServiceScopeFactory>().CreateAsyncScope()
                : null;

            // Store the original paths so the app can check it.
            context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(new StatusCodeReExecuteFeature()
            {
                OriginalPathBase = context.HttpContext.Request.PathBase.Value!,
                OriginalPath = originalPath.Value!,
                OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,
                OriginalStatusCode = originalStatusCode,
                Endpoint = context.HttpContext.GetEndpoint(),
                RouteValues = routeValuesFeature?.RouteValues
            });

            if (scope.HasValue)
            {
                context.HttpContext.RequestServices = scope.Value.ServiceProvider;
            }

            // An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset
            // the endpoint and route values to ensure things are re-calculated.
            HttpExtensions.ClearEndpoint(context.HttpContext);

            context.HttpContext.Request.Path = newPath;
            context.HttpContext.Request.QueryString = newQueryString;
            try
            {
                if (next is not null)
                {
                    await next(context.HttpContext);
                }
                else
                {
                    await context.Next(context.HttpContext);
                }
            }
            finally
            {
                context.HttpContext.Request.QueryString = originalQueryString;
                context.HttpContext.Request.Path = originalPath;
                context.HttpContext.Features.Set<IStatusCodeReExecuteFeature?>(null);
                if (oldScope != null)
                {
                    context.HttpContext.RequestServices = oldScope;
                }
            }
        };

        return handler;
    }
}
