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

using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;

namespace Microsoft.AspNetCore.Builder;

/// <summary>
/// Extensions for <see cref="IEndpointRouteBuilder"/>.
/// </summary>
public static class ComponentEndpointRouteBuilderExtensions
{
    /// <summary>
    /// Maps the Blazor <see cref="Hub" /> to the default path.
    /// </summary>
    /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
    /// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
    public static ComponentEndpointConventionBuilder MapBlazorHub(this IEndpointRouteBuilder endpoints)
    {
        ArgumentNullException.ThrowIfNull(endpoints);

        return endpoints.MapBlazorHub(ComponentHub.DefaultPath);
    }

    /// <summary>
    /// Maps the Blazor <see cref="Hub" /> to the path <paramref name="path"/>.
    /// </summary>
    /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
    /// <param name="path">The path to map the Blazor <see cref="Hub" />.</param>
    /// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
    public static ComponentEndpointConventionBuilder MapBlazorHub(
        this IEndpointRouteBuilder endpoints,
        string path)
    {
        ArgumentNullException.ThrowIfNull(endpoints);
        ArgumentNullException.ThrowIfNull(path);

        return endpoints.MapBlazorHub(path, configureOptions: _ => { });
    }

    /// <summary>
    ///Maps the Blazor <see cref="Hub" /> to the default path.
    /// </summary>
    /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
    /// <param name="configureOptions">A callback to configure dispatcher options.</param>
    /// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
    public static ComponentEndpointConventionBuilder MapBlazorHub(
        this IEndpointRouteBuilder endpoints,
        Action<HttpConnectionDispatcherOptions> configureOptions)
    {
        ArgumentNullException.ThrowIfNull(endpoints);
        ArgumentNullException.ThrowIfNull(configureOptions);

        return endpoints.MapBlazorHub(ComponentHub.DefaultPath, configureOptions);
    }

    /// <summary>
    /// Maps the Blazor <see cref="Hub" /> to the path <paramref name="path"/>.
    /// </summary>
    /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
    /// <param name="path">The path to map the Blazor <see cref="Hub" />.</param>
    /// <param name="configureOptions">A callback to configure dispatcher options.</param>
    /// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
    public static ComponentEndpointConventionBuilder MapBlazorHub(
        this IEndpointRouteBuilder endpoints,
        string path,
        Action<HttpConnectionDispatcherOptions> configureOptions)
    {
        ArgumentNullException.ThrowIfNull(endpoints);
        ArgumentNullException.ThrowIfNull(path);
        ArgumentNullException.ThrowIfNull(configureOptions);

        var hubEndpoint = endpoints.MapHub<ComponentHub>(path, configureOptions);

        var disconnectEndpoint = endpoints.Map(
            (path.EndsWith('/') ? path : path + "/") + "disconnect/",
            endpoints.CreateApplicationBuilder().UseMiddleware<CircuitDisconnectMiddleware>().Build())
            .WithDisplayName("Blazor disconnect");

        var jsInitializersEndpoint = endpoints.Map(
            (path.EndsWith('/') ? path : path + "/") + "initializers/",
            endpoints.CreateApplicationBuilder().UseMiddleware<CircuitJavaScriptInitializationMiddleware>().Build())
            .WithDisplayName("Blazor initializers");

        var blazorEndpoint = GetBlazorEndpoint(endpoints);

        return new ComponentEndpointConventionBuilder(hubEndpoint, disconnectEndpoint, jsInitializersEndpoint, blazorEndpoint);
    }

    private static IEndpointConventionBuilder GetBlazorEndpoint(IEndpointRouteBuilder endpoints)
    {
        var options = new StaticFileOptions
        {
            FileProvider = new ManifestEmbeddedFileProvider(typeof(ComponentEndpointRouteBuilderExtensions).Assembly),
            OnPrepareResponse = CacheHeaderSettings.SetCacheHeaders
        };

        var app = endpoints.CreateApplicationBuilder();
        app.Use(next => context =>
        {
            // Set endpoint to null so the static files middleware will handle the request.
            context.SetEndpoint(null);

            return next(context);
        });
        app.UseStaticFiles(options);

        var blazorEndpoint = endpoints.Map("/_framework/blazor.server.js", app.Build())
            .WithDisplayName("Blazor static files");

        blazorEndpoint.Add((builder) => ((RouteEndpointBuilder)builder).Order = int.MinValue);
        
#if DEBUG
        // We only need to serve the sourcemap when working on the framework, not in the distributed packages
        endpoints.Map("/_framework/blazor.server.js.map", app.Build())
            .WithDisplayName("Blazor static files sourcemap")
            .Add((builder) => ((RouteEndpointBuilder)builder).Order = int.MinValue);
#endif

        return blazorEndpoint;
    }
}
