using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Configuration;
using ZB.MOM.WW.GalaxyRepository.DependencyInjection;
using ZB.MOM.WW.Health;
using ZB.MOM.WW.MxGateway.Contracts;
using ZB.MOM.WW.MxGateway.Server.Alarms;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Dashboard;
using ZB.MOM.WW.MxGateway.Server.Diagnostics;
using ZB.MOM.WW.MxGateway.Server.Grpc;
using ZB.MOM.WW.MxGateway.Server.Metrics;
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;
using ZB.MOM.WW.MxGateway.Server.Sessions;
using ZB.MOM.WW.MxGateway.Server.Workers;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Configuration;
using ZB.MOM.WW.Secrets.DependencyInjection;
using ZB.MOM.WW.Secrets.Sqlite;
using ZB.MOM.WW.Secrets.Ui;
using ZB.MOM.WW.Telemetry;
using ZB.MOM.WW.Telemetry.Serilog;

namespace ZB.MOM.WW.MxGateway.Server;

/// <summary>
/// Configures and builds the gateway web application.
/// </summary>
public static class GatewayApplication
{
    private const string StaticAssetsManifestFileName = "ZB.MOM.WW.MxGateway.Server.staticwebassets.endpoints.json";

    /// <summary>
    /// Builds a configured web application with all gateway services and middleware.
    /// </summary>
    /// <param name="args">Command-line arguments passed to the application.</param>
    /// <returns>A configured web application ready to run.</returns>
    public static WebApplication Build(string[] args)
    {
        WebApplicationBuilder builder = CreateBuilder(args);
        WebApplication app = builder.Build();

        app.UseGatewayRequestLoggingScope();
        app.UseStaticFiles();
        app.UseAuthentication();
        app.UseAuthorization();
        // The rate limiter must run after routing has selected the endpoint (so its
        // RequireRateLimiting policy metadata is visible) and before the endpoint executes.
        app.UseRateLimiter();
        app.UseAntiforgery();
        app.MapGatewayEndpoints();

        return app;
    }

    /// <summary>
    /// Creates a web application builder configured with gateway services.
    /// </summary>
    /// <param name="args">Command-line arguments passed to the application.</param>
    /// <returns>A configured web application builder.</returns>
    public static WebApplicationBuilder CreateBuilder(string[] args)
    {
        WebApplicationBuilder builder = WebApplication.CreateBuilder(new WebApplicationOptions
        {
            Args = args,
            ContentRootPath = ResolveContentRootPath(),
        });
        StaticWebAssetsLoader.UseStaticWebAssets(builder.Environment, builder.Configuration);

        ApplyDefaultSecretsStorePath(builder.Configuration);

        // Resolve ${secret:...} references in configuration BEFORE any config consumer (TLS, Kestrel,
        // GatewayOptions/Ldap/Galaxy validators) reads a value, using a standalone secrets provider
        // (envelope-decrypted via the master key). A token referencing a missing secret fails fast
        // here (SecretNotFoundException); config with no tokens is untouched (no-op), so this is safe
        // to always run. CreateBuilder is synchronous and single-shot at bootstrap, so the two awaits
        // are driven via GetAwaiter().GetResult() (no sync-context deadlock risk during host startup).
        // The content root is passed explicitly because this container is a throwaway
        // ServiceCollection with no IHostEnvironment in it. Without it the library cannot tell
        // "no content root exists" from "no host is registered", so it skips the
        // under-content-root rule — and the migrator below CREATES the store before the real host
        // ever validates. The boot then fails a moment later, having already left an empty
        // database with its -wal/-shm siblings at the very path the rule rejects. That artifact is
        // what made the 2026-08-09 outage read as "the database is there, it's just empty".
        // DO NOT simplify this to the 3-argument overload: it still compiles, the app still boots
        // when the path is correct, and the guard silently stops running at the one moment that
        // matters.
#pragma warning disable ASP0000 // deliberate throwaway container, disposed here, shares no singletons
        using (var secretsProvider = new ServiceCollection()
            .AddZbSecrets(builder.Configuration, "Secrets", builder.Environment.ContentRootPath)
            .BuildServiceProvider())
#pragma warning restore ASP0000
        {
            // Intentionally unconditional: the pre-host expander below needs the secrets
            // store schema to exist before the first ${secret:} resolve. Secrets:RunMigrationsOnStartup
            // governs only the runtime SecretsMigrationHostedService, not this bootstrap path.
            secretsProvider.GetRequiredService<SqliteSecretsStoreMigrator>()
                .MigrateAsync(default).GetAwaiter().GetResult();
            var resolver = secretsProvider.GetRequiredService<ISecretResolver>();
            new SecretReferenceExpander(resolver)
                .ExpandConfigurationAsync((IConfigurationRoot)builder.Configuration, default)
                .GetAwaiter().GetResult();
        }

        ConfigureSelfSignedTls(builder);

        builder.AddZbSerilog(o => o.ServiceName = "mxgateway");

        builder.Services.AddGatewayConfiguration(builder.Configuration);
        builder.Services.AddZbSecrets(builder.Configuration, "Secrets");
        builder.Services.AddSqliteAuthStore(builder.Configuration);
        builder.Services.AddGatewayGrpcAuthorization();
        AddLoginRateLimiter(builder);
        builder.Services.AddHealthChecks()
            .AddTypeActivatedCheck<AuthStoreHealthCheck>(
                "auth-store",
                failureStatus: null,
                tags: new[] { ZbHealthTags.Ready })
            // Active, not Ready: a gateway holding no sessions is legitimately ready to serve.
            // See SessionHealthCheck for why zero sessions is healthy.
            .AddTypeActivatedCheck<SessionHealthCheck>(
                "mxaccess-sessions",
                failureStatus: null,
                tags: new[] { ZbHealthTags.Active });
        builder.Services.AddSingleton<GatewayMetrics>();
        builder.AddZbTelemetry(o =>
        {
            o.ServiceName = "mxgateway";
            o.Meters = [GatewayMetrics.MeterName]; // "MxGateway.Server" — name unchanged
            if (Enum.TryParse<ZbExporter>(builder.Configuration["MxGateway:Telemetry:Exporter"], ignoreCase: true, out var exporter))
                o.Exporter = exporter;
            var otlp = builder.Configuration["MxGateway:Telemetry:OtlpEndpoint"];
            if (!string.IsNullOrWhiteSpace(otlp))
                o.OtlpEndpoint = otlp;
        });
        builder.Services.AddSingleton<ILogRedactor, GatewayLogRedactorSeam>();
        builder.Services.AddSingleton<MxAccessGrpcMapper>();
        builder.Services.AddSingleton<MxAccessGrpcRequestValidator>();
        builder.Services.AddSingleton<IEventStreamService, EventStreamService>();
        builder.Services.AddWorkerProcessLauncher();
        builder.Services.AddGatewaySessions();
        builder.Services.AddGatewayAlarms();
        builder.Services.AddGatewayDashboard(builder.Configuration);
        // Register the shared Secrets UI authorization policies (secrets:manage + secrets:reveal)
        // additively so they compose with the dashboard's existing AddAuthorization block. The
        // mounted /admin/secrets Blazor page carries [Authorize(Policy = "secrets:manage")].
        builder.Services.Configure<AuthorizationOptions>(o => o.AddSecretsAuthorization());
        // Register the gateway's browse-scope provider before AddZbGalaxyRepository so the
        // library's TryAddSingleton default (NullGalaxyBrowseScopeProvider) does not win.
        builder.Services.AddSingleton<ZB.MOM.WW.GalaxyRepository.Grpc.IGalaxyBrowseScopeProvider,
            Security.Authorization.GatewayBrowseScopeProvider>();

        // The Galaxy package binds GalaxyRepositoryOptions but ships no validator or default for the
        // snapshot path (A2 handoff): the gateway owns both because it is the process that writes the
        // snapshot. GalaxyRepositoryOptions.SnapshotCachePath is init-only, so the default cannot be
        // applied via PostConfigure — supply it as a configuration value (before the bind) when the
        // shipped config leaves it blank. It resolves to the per-OS CommonApplicationData location,
        // byte-identical to the removed appsettings literal on Windows (SEC-33).
        if (string.IsNullOrWhiteSpace(builder.Configuration["MxGateway:Galaxy:SnapshotCachePath"]))
        {
            builder.Configuration["MxGateway:Galaxy:SnapshotCachePath"] = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
                "MxGateway",
                "galaxy-snapshot.json");
        }

        builder.Services.AddZbGalaxyRepository(builder.Configuration, "MxGateway:Galaxy");

        // Validate that persistence has a valid, host-rooted snapshot path (SEC-33).
        builder.Services.AddSingleton<
            Microsoft.Extensions.Options.IValidateOptions<ZB.MOM.WW.GalaxyRepository.GalaxyRepositoryOptions>,
            Configuration.GalaxyRepositoryOptionsValidator>();
        builder.Services.AddOptions<ZB.MOM.WW.GalaxyRepository.GalaxyRepositoryOptions>().ValidateOnStart();

        return builder;
    }

    // Registers the named fixed-window rate-limiter policy applied to POST /auth/login. The
    // limit knobs are read from MxGateway:Security at startup; the per-request partition is keyed on
    // the remote IP (see DashboardEndpointRouteBuilderExtensions.GetLoginRateLimitPartition).
    private static void AddLoginRateLimiter(WebApplicationBuilder builder)
    {
        SecurityOptions security =
            builder.Configuration.GetSection(SecurityOptions.SectionName).Get<SecurityOptions>()
            ?? new SecurityOptions();

        builder.Services.AddRateLimiter(options =>
        {
            options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
            options.AddPolicy(
                DashboardEndpointRouteBuilderExtensions.LoginRateLimiterPolicy,
                httpContext => DashboardEndpointRouteBuilderExtensions.GetLoginRateLimitPartition(
                    httpContext,
                    security));
        });
    }

    /// <summary>
    /// Supplies the default location of the encrypted secrets store when nothing configured one.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The store used to default to a bare relative <c>mxgateway-secrets.db</c>, which resolves
    /// against the working directory and therefore normally lands inside the application directory.
    /// That is the shape that lost every API key on a production host: the upgrade procedure renames
    /// the application directory away, the store goes with it, and a fresh empty one appears in its
    /// place with no error. In development the same default writes a database into the source tree.
    /// </para>
    /// <para>
    /// This sets a default for an <em>unset</em> key; it never relocates a value someone configured.
    /// That distinction matters — <see cref="Configuration.GatewayConfigPathRules"/> deliberately
    /// rejects bad configured paths rather than quietly moving them, because silently relocating a
    /// credential store is worse than a boot error. Choosing where to put a value nobody specified
    /// is a different act from overriding one they did.
    /// </para>
    /// <para>
    /// The location mirrors <c>AuthenticationOptions.SqlitePath</c> so both gateway stores sit
    /// together, and the mechanism is the one SEC-33 already used for
    /// <c>MxGateway:Galaxy:SnapshotCachePath</c> below — same problem, same fix, same file. It also
    /// matches what <c>docs/GatewayConfiguration.md</c> already tells operators to
    /// pass to the <c>secret</c> CLI — an absolute default also removes the CLI/gateway divergence
    /// that a working-directory-relative path can cause. On non-Windows hosts
    /// <see cref="Environment.SpecialFolder.CommonApplicationData"/> is typically not writable by a
    /// normal user, so a local run there must set <c>Secrets__SqlitePath</c> explicitly, exactly as
    /// it already must for the auth store.
    /// </para>
    /// <para>
    /// <b>This deliberately differs from the <c>ZB.MOM.WW.Secrets</c> library default</b>, which is
    /// <see cref="Environment.SpecialFolder.LocalApplicationData"/>-derived so the family's
    /// cross-platform apps still boot locally without an override. The gateway keeps
    /// <c>CommonApplicationData</c> because it runs as a machine-wide Windows service and its other
    /// two stores — the auth database and the Galaxy snapshot — already live there; splitting them
    /// would be the greater inconsistency. The value set here always wins, so the library default is
    /// unreachable in this app. Do not "fix" the difference by deleting this method: that would
    /// silently move the store, which is the failure this whole rule exists to prevent.
    /// </para>
    /// </remarks>
    /// <param name="configuration">The configuration to supply the default into.</param>
    private static void ApplyDefaultSecretsStorePath(IConfiguration configuration)
    {
        if (!string.IsNullOrWhiteSpace(configuration["Secrets:SqlitePath"]))
        {
            return;
        }

        configuration["Secrets:SqlitePath"] = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
            "MxGateway",
            "mxgateway-secrets.db");
    }

    private static void ConfigureSelfSignedTls(WebApplicationBuilder builder)
    {
        if (!Security.Tls.KestrelTlsInspector.RequiresGeneratedCertificate(builder.Configuration))
        {
            return;
        }

        Configuration.TlsOptions tlsOptions =
            builder.Configuration.GetSection("MxGateway:Tls").Get<Configuration.TlsOptions>()
            ?? new Configuration.TlsOptions();

        using ILoggerFactory loggerFactory = LoggerFactory.Create(logging =>
        {
            logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
            logging.AddConsole();
        });
        Security.Tls.SelfSignedCertificateProvider provider = new(
            tlsOptions,
            loggerFactory.CreateLogger<Security.Tls.SelfSignedCertificateProvider>(),
            TimeProvider.System);

        X509Certificate2 certificate = provider.LoadOrCreate();
        builder.WebHost.ConfigureKestrel(options =>
            // The certificate is intentionally owned by Kestrel for the application
            // lifetime; it is not disposed here.
            options.ConfigureHttpsDefaults(https => https.ServerCertificate = certificate));
    }

    private static string ResolveContentRootPath()
    {
        string? configuredContentRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_CONTENTROOT");
        if (!string.IsNullOrWhiteSpace(configuredContentRootPath)
            && IsServerContentRoot(configuredContentRootPath))
        {
            return configuredContentRootPath;
        }

        string currentDirectory = Environment.CurrentDirectory;
        if (IsServerContentRoot(currentDirectory))
        {
            return currentDirectory;
        }

        string baseDirectory = AppContext.BaseDirectory;
        if (IsServerContentRoot(baseDirectory))
        {
            return baseDirectory;
        }

        string? discoveredContentRootPath = DiscoverServerContentRoot(currentDirectory)
            ?? DiscoverServerContentRoot(baseDirectory);

        return discoveredContentRootPath ?? baseDirectory;
    }

    private static string? DiscoverServerContentRoot(string startPath)
    {
        DirectoryInfo? directory = new(startPath);
        while (directory is not null)
        {
            if (IsServerContentRoot(directory.FullName))
            {
                return directory.FullName;
            }

            string serverProjectPath = Path.Combine(directory.FullName, "src", "ZB.MOM.WW.MxGateway.Server");
            if (IsServerContentRoot(serverProjectPath))
            {
                return serverProjectPath;
            }

            directory = directory.Parent;
        }

        return null;
    }

    private static bool IsServerContentRoot(string path)
    {
        return File.Exists(Path.Combine(path, "appsettings.json"))
            && Directory.Exists(Path.Combine(path, "wwwroot"));
    }

    /// <summary>
    /// Maps gateway endpoints including gRPC services, health checks, and the dashboard.
    /// </summary>
    /// <param name="endpoints">Endpoint route builder to map endpoints to.</param>
    /// <returns>The same endpoint route builder for chaining.</returns>
    public static IEndpointRouteBuilder MapGatewayEndpoints(this IEndpointRouteBuilder endpoints)
    {
        endpoints.MapStaticAssets(ResolveStaticAssetsManifestPath());

        endpoints.MapZbHealth();
        endpoints.MapZbMetrics();

        endpoints.MapGrpcService<MxAccessGatewayService>();
        endpoints.MapZbGalaxyRepository();
        endpoints.MapGatewayDashboard();

        return endpoints;
    }

    private static string ResolveStaticAssetsManifestPath()
    {
        string manifestPath = Path.Combine(AppContext.BaseDirectory, StaticAssetsManifestFileName);

        return File.Exists(manifestPath) ? manifestPath : StaticAssetsManifestFileName;
    }
}
