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

#nullable enable

using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Infrastructure;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Hosting;

/// <summary>
/// Contains extensions for configuring an <see cref="IWebHostBuilder" />.
/// </summary>
public static class WebHostBuilderExtensions
{
    /// <summary>
    /// Specify the startup method to be used to configure the web application.
    /// </summary>
    /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
    /// <param name="configureApp">The delegate that configures the <see cref="IApplicationBuilder"/>.</param>
    /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
    public static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Action<IApplicationBuilder> configureApp)
    {
        ArgumentNullException.ThrowIfNull(configureApp);

        // Light up the ISupportsStartup implementation
        if (hostBuilder is ISupportsStartup supportsStartup)
        {
            return supportsStartup.Configure(configureApp);
        }

        var startupAssemblyName = configureApp.GetMethodInfo().DeclaringType!.Assembly.GetName().Name!;

        hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);

        return hostBuilder.ConfigureServices((context, services) =>
        {
            services.AddSingleton<IStartup>(sp =>
            {
                return new DelegateStartup(sp.GetRequiredService<IServiceProviderFactory<IServiceCollection>>(), (app => configureApp(app)));
            });
        });
    }

    /// <summary>
    /// Specify the startup method to be used to configure the web application.
    /// </summary>
    /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
    /// <param name="configureApp">The delegate that configures the <see cref="IApplicationBuilder"/>.</param>
    /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
    public static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Action<WebHostBuilderContext, IApplicationBuilder> configureApp)
    {
        ArgumentNullException.ThrowIfNull(configureApp);

        // Light up the ISupportsStartup implementation
        if (hostBuilder is ISupportsStartup supportsStartup)
        {
            return supportsStartup.Configure(configureApp);
        }

        var startupAssemblyName = configureApp.GetMethodInfo().DeclaringType!.Assembly.GetName().Name!;

        hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);

        return hostBuilder.ConfigureServices((context, services) =>
        {
            services.AddSingleton<IStartup>(sp =>
            {
                return new DelegateStartup(sp.GetRequiredService<IServiceProviderFactory<IServiceCollection>>(), (app => configureApp(context, app)));
            });
        });
    }

    /// <summary>
    /// Specify a factory that creates the startup instance to be used by the web host.
    /// </summary>
    /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
    /// <param name="startupFactory">A delegate that specifies a factory for the startup class.</param>
    /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
    /// <remarks>When in a trimmed app, all public methods of <typeparamref name="TStartup"/> are preserved. This should match the Startup type directly (and not a base type).</remarks>
    public static IWebHostBuilder UseStartup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TStartup>(this IWebHostBuilder hostBuilder, Func<WebHostBuilderContext, TStartup> startupFactory) where TStartup : class
    {
        ArgumentNullException.ThrowIfNull(startupFactory);

        // Light up the GenericWebHostBuilder implementation
        if (hostBuilder is ISupportsStartup supportsStartup)
        {
            return supportsStartup.UseStartup(startupFactory);
        }

        var startupAssemblyName = startupFactory.GetMethodInfo().DeclaringType!.Assembly.GetName().Name;

        hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);

        return hostBuilder
            .ConfigureServices((context, services) =>
            {
                services.AddSingleton(typeof(IStartup), GetStartupInstance);

                [UnconditionalSuppressMessage("Trimmer", "IL2072", Justification = "Startup type created by factory can't be determined statically.")]
                object GetStartupInstance(IServiceProvider serviceProvider)
                {
                    var instance = startupFactory(context) ?? throw new InvalidOperationException("The specified factory returned null startup instance.");

                    var hostingEnvironment = serviceProvider.GetRequiredService<IHostEnvironment>();

                    // Check if the instance implements IStartup before wrapping
                    if (instance is IStartup startup)
                    {
                        return startup;
                    }

                    return new ConventionBasedStartup(StartupLoader.LoadMethods(serviceProvider, instance.GetType(), hostingEnvironment.EnvironmentName, instance));
                }
            });
    }

    /// <summary>
    /// Specify the startup type to be used by the web host.
    /// </summary>
    /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
    /// <param name="startupType">The <see cref="Type"/> to be used.</param>
    /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
    public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, [DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] Type startupType)
    {
        ArgumentNullException.ThrowIfNull(startupType);

        // Light up the GenericWebHostBuilder implementation
        if (hostBuilder is ISupportsStartup supportsStartup)
        {
            return supportsStartup.UseStartup(startupType);
        }

        var startupAssemblyName = startupType.Assembly.GetName().Name;

        hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);

        return hostBuilder
            .ConfigureServices(services =>
            {
                if (typeof(IStartup).IsAssignableFrom(startupType))
                {
                    services.AddSingleton(typeof(IStartup), startupType);
                }
                else
                {
                    services.AddSingleton(typeof(IStartup), sp =>
                    {
                        var hostingEnvironment = sp.GetRequiredService<IHostEnvironment>();
                        return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName));
                    });
                }
            });
    }

    /// <summary>
    /// Specify the startup type to be used by the web host.
    /// </summary>
    /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
    /// <typeparam name ="TStartup">The type containing the startup methods for the application.</typeparam>
    /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
    public static IWebHostBuilder UseStartup<[DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class
    {
        return hostBuilder.UseStartup(typeof(TStartup));
    }

    /// <summary>
    /// Configures the default service provider
    /// </summary>
    /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
    /// <param name="configure">A callback used to configure the <see cref="ServiceProviderOptions"/> for the default <see cref="IServiceProvider"/>.</param>
    /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
    public static IWebHostBuilder UseDefaultServiceProvider(this IWebHostBuilder hostBuilder, Action<ServiceProviderOptions> configure)
    {
        return hostBuilder.UseDefaultServiceProvider((context, options) => configure(options));
    }

    /// <summary>
    /// Configures the default service provider
    /// </summary>
    /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
    /// <param name="configure">A callback used to configure the <see cref="ServiceProviderOptions"/> for the default <see cref="IServiceProvider"/>.</param>
    /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
    public static IWebHostBuilder UseDefaultServiceProvider(this IWebHostBuilder hostBuilder, Action<WebHostBuilderContext, ServiceProviderOptions> configure)
    {
        // Light up the GenericWebHostBuilder implementation
        if (hostBuilder is ISupportsUseDefaultServiceProvider supportsDefaultServiceProvider)
        {
            return supportsDefaultServiceProvider.UseDefaultServiceProvider(configure);
        }

        return hostBuilder.ConfigureServices((context, services) =>
        {
            var options = new ServiceProviderOptions();
            configure(context, options);
            services.Replace(ServiceDescriptor.Singleton<IServiceProviderFactory<IServiceCollection>>(new DefaultServiceProviderFactory(options)));
        });
    }

    /// <summary>
    /// Adds a delegate for configuring the <see cref="IConfigurationBuilder"/> that will construct an <see cref="IConfiguration"/>.
    /// </summary>
    /// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
    /// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder" /> that will be used to construct an <see cref="IConfiguration" />.</param>
    /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
    /// <remarks>
    /// The <see cref="IConfiguration"/> and <see cref="ILoggerFactory"/> on the <see cref="WebHostBuilderContext"/> are uninitialized at this stage.
    /// The <see cref="IConfigurationBuilder"/> is pre-populated with the settings of the <see cref="IWebHostBuilder"/>.
    /// </remarks>
    public static IWebHostBuilder ConfigureAppConfiguration(this IWebHostBuilder hostBuilder, Action<IConfigurationBuilder> configureDelegate)
    {
        return hostBuilder.ConfigureAppConfiguration((context, builder) => configureDelegate(builder));
    }

    /// <summary>
    /// Adds a delegate for configuring the provided <see cref="ILoggingBuilder"/>. This may be called multiple times.
    /// </summary>
    /// <param name="hostBuilder">The <see cref="IWebHostBuilder" /> to configure.</param>
    /// <param name="configureLogging">The delegate that configures the <see cref="ILoggingBuilder"/>.</param>
    /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
    public static IWebHostBuilder ConfigureLogging(this IWebHostBuilder hostBuilder, Action<ILoggingBuilder> configureLogging)
    {
        return hostBuilder.ConfigureServices(collection => collection.AddLogging(configureLogging));
    }

    /// <summary>
    /// Adds a delegate for configuring the provided <see cref="LoggerFactory"/>. This may be called multiple times.
    /// </summary>
    /// <param name="hostBuilder">The <see cref="IWebHostBuilder" /> to configure.</param>
    /// <param name="configureLogging">The delegate that configures the <see cref="LoggerFactory"/>.</param>
    /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
    public static IWebHostBuilder ConfigureLogging(this IWebHostBuilder hostBuilder, Action<WebHostBuilderContext, ILoggingBuilder> configureLogging)
    {
        return hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(context, builder)));
    }

    /// <summary>
    /// Configures the <see cref="IWebHostEnvironment.WebRootFileProvider"/> to use static web assets
    /// defined by referenced projects and packages.
    /// </summary>
    /// <param name="builder">The <see cref="IWebHostBuilder"/>.</param>
    /// <returns>The <see cref="IWebHostBuilder"/>.</returns>
    public static IWebHostBuilder UseStaticWebAssets(this IWebHostBuilder builder)
    {
        builder.ConfigureAppConfiguration((context, configBuilder) =>
        {
            StaticWebAssetsLoader.UseStaticWebAssets(context.HostingEnvironment, context.Configuration);
        });

        return builder;
    }
}
