// 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;
using System.Linq;
using Microsoft.AspNetCore.Hosting.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Hosting;

internal sealed partial class GenericWebHostService : IHostedService
{
    public GenericWebHostService(IOptions<GenericWebHostServiceOptions> options,
                                 IServer server,
                                 ILoggerFactory loggerFactory,
                                 DiagnosticListener diagnosticListener,
                                 ActivitySource activitySource,
                                 DistributedContextPropagator propagator,
                                 IHttpContextFactory httpContextFactory,
                                 IApplicationBuilderFactory applicationBuilderFactory,
                                 IEnumerable<IStartupFilter> startupFilters,
                                 IConfiguration configuration,
                                 IWebHostEnvironment hostingEnvironment,
                                 HostingMetrics hostingMetrics)
    {
        Options = options.Value;
        Server = server;
        Logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Hosting.Diagnostics");
        LifetimeLogger = loggerFactory.CreateLogger("Microsoft.Hosting.Lifetime");
        DiagnosticListener = diagnosticListener;
        ActivitySource = activitySource;
        Propagator = propagator;
        HttpContextFactory = httpContextFactory;
        ApplicationBuilderFactory = applicationBuilderFactory;
        StartupFilters = startupFilters;
        Configuration = configuration;
        HostingEnvironment = hostingEnvironment;
        HostingMetrics = hostingMetrics;
    }

    public GenericWebHostServiceOptions Options { get; }
    public IServer Server { get; }
    public ILogger Logger { get; }
    // Only for high level lifetime events
    public ILogger LifetimeLogger { get; }
    public DiagnosticListener DiagnosticListener { get; }
    public ActivitySource ActivitySource { get; }
    public DistributedContextPropagator Propagator { get; }
    public IHttpContextFactory HttpContextFactory { get; }
    public IApplicationBuilderFactory ApplicationBuilderFactory { get; }
    public IEnumerable<IStartupFilter> StartupFilters { get; }
    public IConfiguration Configuration { get; }
    public IWebHostEnvironment HostingEnvironment { get; }
    public HostingMetrics HostingMetrics { get; }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        HostingEventSource.Log.HostStart();

        var serverAddressesFeature = Server.Features.Get<IServerAddressesFeature>();
        var addresses = serverAddressesFeature?.Addresses;
        if (addresses != null && !addresses.IsReadOnly && addresses.Count == 0)
        {
            // We support reading "urls" from app configuration
            var urls = Configuration[WebHostDefaults.ServerUrlsKey];

            // But fall back to host settings
            if (string.IsNullOrEmpty(urls))
            {
                urls = Options.WebHostOptions.ServerUrls;
            }

            var httpPorts = Configuration[WebHostDefaults.HttpPortsKey] ?? string.Empty;
            var httpsPorts = Configuration[WebHostDefaults.HttpsPortsKey] ?? string.Empty;
            if (string.IsNullOrEmpty(urls))
            {
                // HTTP_PORTS and HTTPS_PORTS, these are lower priority than Urls.
                static string ExpandPorts(string ports, string scheme)
                {
                    return string.Join(';',
                        ports.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
                        .Select(port => $"{scheme}://*:{port}"));
                }

                var httpUrls = ExpandPorts(httpPorts, Uri.UriSchemeHttp);
                var httpsUrls = ExpandPorts(httpsPorts, Uri.UriSchemeHttps);
                urls = $"{httpUrls};{httpsUrls}";
            }
            else if (!string.IsNullOrEmpty(httpPorts) || !string.IsNullOrEmpty(httpsPorts))
            {
                Logger.PortsOverridenByUrls(httpPorts, httpsPorts, urls);
            }

            if (!string.IsNullOrEmpty(urls))
            {
                // We support reading "preferHostingUrls" from app configuration
                var preferHostingUrlsConfig = Configuration[WebHostDefaults.PreferHostingUrlsKey];

                // But fall back to host settings
                if (!string.IsNullOrEmpty(preferHostingUrlsConfig))
                {
                    serverAddressesFeature!.PreferHostingUrls = WebHostUtilities.ParseBool(preferHostingUrlsConfig);
                }
                else
                {
                    serverAddressesFeature!.PreferHostingUrls = Options.WebHostOptions.PreferHostingUrls;
                }

                foreach (var value in urls.Split(';', StringSplitOptions.RemoveEmptyEntries))
                {
                    addresses.Add(value);
                }
            }
        }

        RequestDelegate? application = null;

        try
        {
            var configure = Options.ConfigureApplication;

            if (configure == null)
            {
                throw new InvalidOperationException($"No application configured. Please specify an application via IWebHostBuilder.UseStartup, IWebHostBuilder.Configure, or specifying the startup assembly via {nameof(WebHostDefaults.StartupAssemblyKey)} in the web host configuration.");
            }

            var builder = ApplicationBuilderFactory.CreateBuilder(Server.Features);

            foreach (var filter in StartupFilters.Reverse())
            {
                configure = filter.Configure(configure);
            }

            configure(builder);

            // Build the request pipeline
            application = builder.Build();
        }
        catch (Exception ex)
        {
            Logger.ApplicationError(ex);

            if (!Options.WebHostOptions.CaptureStartupErrors)
            {
                throw;
            }

            var showDetailedErrors = HostingEnvironment.IsDevelopment() || Options.WebHostOptions.DetailedErrors;

            application = ErrorPageBuilder.BuildErrorPageApplication(HostingEnvironment.ContentRootFileProvider, Logger, showDetailedErrors, ex);
        }

        var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory, HostingEventSource.Log, HostingMetrics);

        await Server.StartAsync(httpApplication, cancellationToken);
        HostingEventSource.Log.ServerReady();

        if (addresses != null)
        {
            foreach (var address in addresses)
            {
                Log.ListeningOnAddress(LifetimeLogger, address);
            }
        }

        if (Logger.IsEnabled(LogLevel.Debug))
        {
            foreach (var assembly in Options.WebHostOptions.GetFinalHostingStartupAssemblies())
            {
                Log.StartupAssemblyLoaded(Logger, assembly);
            }
        }

        if (Options.HostingStartupExceptions != null)
        {
            foreach (var exception in Options.HostingStartupExceptions.InnerExceptions)
            {
                Logger.HostingStartupAssemblyError(exception);
            }
        }
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        try
        {
            await Server.StopAsync(cancellationToken);
        }
        finally
        {
            HostingEventSource.Log.HostStop();
        }
    }

    private static partial class Log
    {
        [LoggerMessage(14, LogLevel.Information,
            "Now listening on: {address}",
            EventName = "ListeningOnAddress")]
        public static partial void ListeningOnAddress(ILogger logger, string address);

        [LoggerMessage(13, LogLevel.Debug,
            "Loaded hosting startup assembly {assemblyName}",
            EventName = "HostingStartupAssemblyLoaded",
            SkipEnabledCheck = true)]
        public static partial void StartupAssemblyLoaded(ILogger logger, string assemblyName);
    }
}
