using System;
using System.Diagnostics;
using System.Globalization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Logging.Abstractions;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
using MongoDB.Driver;
using Serilog;
using Serilog.Events;
using StellaOps.Authority;
using StellaOps.Authority.Audit;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Plugins;
using StellaOps.Authority.Bootstrap;
using StellaOps.Authority.Storage.Mongo.Extensions;
using StellaOps.Authority.Storage.Mongo.Initialization;
using StellaOps.Authority.Storage.Mongo.Stores;
using StellaOps.Authority.RateLimiting;
using StellaOps.Configuration;
using StellaOps.Plugin.DependencyInjection;
using StellaOps.Plugin.Hosting;
using StellaOps.Authority.OpenIddict.Handlers;
using System.Linq;
using StellaOps.Cryptography.Audit;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Authority.Revocation;
using StellaOps.Authority.Signing;
using StellaOps.Cryptography;
using StellaOps.Authority.Storage.Mongo.Documents;

var builder = WebApplication.CreateBuilder(args);

Activity.DefaultIdFormat = ActivityIdFormat.W3C;
Activity.ForceDefaultIdFormat = true;

AuthorityTelemetryConfiguration.Configure(builder);

var authorityConfiguration = StellaOpsAuthorityConfiguration.Build(options =>
{
    options.BasePath = builder.Environment.ContentRootPath;
    options.EnvironmentPrefix = "STELLAOPS_AUTHORITY_";
    options.ConfigureBuilder = configurationBuilder =>
    {
        var contentRoot = builder.Environment.ContentRootPath;
        foreach (var relative in new[]
        {
            "../etc/authority.yaml",
            "../etc/authority.local.yaml",
            "authority.yaml",
            "authority.local.yaml"
        })
        {
            var path = Path.Combine(contentRoot, relative);
            configurationBuilder.AddYamlFile(path, optional: true);
        }
    };
});

builder.Configuration.AddConfiguration(authorityConfiguration.Configuration);

builder.Host.UseSerilog((context, _, loggerConfiguration) =>
{
    loggerConfiguration
        .ReadFrom.Configuration(context.Configuration)
        .Enrich.FromLogContext()
        .MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Warning)
        .WriteTo.Console();
});

var authorityOptions = authorityConfiguration.Options;
var issuer = authorityOptions.Issuer ?? throw new InvalidOperationException("Authority issuer configuration is required.");
builder.Services.AddSingleton(authorityOptions);
builder.Services.AddSingleton<IOptions<StellaOpsAuthorityOptions>>(Options.Create(authorityOptions));
builder.Services.AddHttpContextAccessor();
builder.Services.TryAddSingleton<TimeProvider>(_ => TimeProvider.System);
builder.Services.TryAddSingleton<IAuthorityRateLimiterMetadataAccessor, AuthorityRateLimiterMetadataAccessor>();
builder.Services.TryAddSingleton<IAuthorityRateLimiterPartitionKeyResolver, DefaultAuthorityRateLimiterPartitionKeyResolver>();

builder.Services.AddRateLimiter(rateLimiterOptions =>
{
    AuthorityRateLimiter.Configure(rateLimiterOptions, authorityOptions);
});

builder.Services.AddStellaOpsCrypto();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthoritySigningKeySource, FileAuthoritySigningKeySource>());
builder.Services.AddSingleton<AuthoritySigningKeyManager>();

AuthorityPluginContext[] pluginContexts = AuthorityPluginConfigurationLoader
    .Load(authorityOptions, builder.Environment.ContentRootPath)
    .ToArray();

builder.Services.AddSingleton<IReadOnlyCollection<AuthorityPluginContext>>(pluginContexts);
builder.Services.AddSingleton<IAuthorityPluginRegistry>(_ => new AuthorityPluginRegistry(pluginContexts));

var pluginHostOptions = BuildPluginHostOptions(authorityOptions, builder.Environment.ContentRootPath);
builder.Services.AddSingleton(pluginHostOptions);
builder.Services.RegisterPluginRoutines(authorityConfiguration.Configuration, pluginHostOptions);

builder.Services.AddAuthorityMongoStorage(storageOptions =>
{
    storageOptions.ConnectionString = authorityOptions.Storage.ConnectionString;
    storageOptions.DatabaseName = authorityOptions.Storage.DatabaseName;
    storageOptions.CommandTimeout = authorityOptions.Storage.CommandTimeout;
});

builder.Services.AddSingleton<IAuthorityIdentityProviderRegistry, AuthorityIdentityProviderRegistry>();
builder.Services.AddSingleton<IAuthEventSink, AuthorityAuditSink>();
builder.Services.AddScoped<ValidatePasswordGrantHandler>();
builder.Services.AddScoped<HandlePasswordGrantHandler>();
builder.Services.AddScoped<ValidateClientCredentialsHandler>();
builder.Services.AddScoped<HandleClientCredentialsHandler>();
builder.Services.AddScoped<ValidateAccessTokenHandler>();
builder.Services.AddScoped<PersistTokensHandler>();
builder.Services.AddScoped<HandleRevocationRequestHandler>();
builder.Services.AddSingleton<RevocationBundleBuilder>();
builder.Services.AddSingleton<RevocationBundleSigner>();
builder.Services.AddSingleton<AuthorityRevocationExportService>();
builder.Services.AddSingleton<AuthorityJwksService>();
builder.Services.AddHostedService<BootstrapInviteCleanupService>();

var pluginRegistrationSummary = AuthorityPluginLoader.RegisterPlugins(
    builder.Services,
    authorityConfiguration.Configuration,
    pluginHostOptions,
    pluginContexts,
    NullLogger.Instance);

builder.Services.AddSingleton(pluginRegistrationSummary);

builder.Services.AddRouting(options => options.LowercaseUrls = true);
builder.Services.AddProblemDetails();
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();

builder.Services.AddOpenIddict()
    .AddServer(options =>
    {
        options.SetIssuer(issuer);
        options.SetTokenEndpointUris("/token");
        options.SetAuthorizationEndpointUris("/authorize");
        options.SetIntrospectionEndpointUris("/introspect");
        options.SetRevocationEndpointUris("/revoke");
        options.SetJsonWebKeySetEndpointUris("/jwks");

        options.AllowPasswordFlow();
        options.AllowClientCredentialsFlow();
        options.AllowRefreshTokenFlow();

        options.SetAccessTokenLifetime(authorityOptions.AccessTokenLifetime);
        options.SetRefreshTokenLifetime(authorityOptions.RefreshTokenLifetime);
        options.SetIdentityTokenLifetime(authorityOptions.IdentityTokenLifetime);
        options.SetAuthorizationCodeLifetime(authorityOptions.AuthorizationCodeLifetime);
        options.SetDeviceCodeLifetime(authorityOptions.DeviceCodeLifetime);

        options.DisableAccessTokenEncryption();
        options.DisableTokenStorage();
        options.DisableAuthorizationStorage();

        options.RegisterScopes(
            OpenIddictConstants.Scopes.OpenId,
            OpenIddictConstants.Scopes.Email,
            OpenIddictConstants.Scopes.Profile,
            OpenIddictConstants.Scopes.OfflineAccess);

        options.AddEphemeralEncryptionKey()
               .AddEphemeralSigningKey();

        var aspNetCoreBuilder = options.UseAspNetCore()
            .EnableAuthorizationEndpointPassthrough()
            .EnableTokenEndpointPassthrough();

        if (builder.Environment.IsDevelopment())
        {
            aspNetCoreBuilder.DisableTransportSecurityRequirement();
        }

        options.AddEventHandler<OpenIddictServerEvents.ValidateTokenRequestContext>(descriptor =>
        {
            descriptor.UseScopedHandler<ValidatePasswordGrantHandler>();
        });

        options.AddEventHandler<OpenIddictServerEvents.HandleTokenRequestContext>(descriptor =>
        {
            descriptor.UseScopedHandler<HandlePasswordGrantHandler>();
        });

        options.AddEventHandler<OpenIddictServerEvents.ValidateTokenRequestContext>(descriptor =>
        {
            descriptor.UseScopedHandler<ValidateClientCredentialsHandler>();
        });

        options.AddEventHandler<OpenIddictServerEvents.HandleTokenRequestContext>(descriptor =>
        {
            descriptor.UseScopedHandler<HandleClientCredentialsHandler>();
        });

        options.AddEventHandler<OpenIddictServerEvents.ValidateTokenContext>(descriptor =>
        {
            descriptor.UseScopedHandler<ValidateAccessTokenHandler>();
        });

        options.AddEventHandler<OpenIddictServerEvents.ProcessSignInContext>(descriptor =>
        {
            descriptor.UseScopedHandler<PersistTokensHandler>();
        });

        options.AddEventHandler<OpenIddictServerEvents.HandleRevocationRequestContext>(descriptor =>
        {
            descriptor.UseScopedHandler<HandleRevocationRequestHandler>();
        });
    });

builder.Services.Configure<OpenIddictServerOptions>(options =>
{
    options.DisableSlidingRefreshTokenExpiration = false;
    options.DisableRollingRefreshTokens = false;
});

var app = builder.Build();

var mongoInitializer = app.Services.GetRequiredService<AuthorityMongoInitializer>();
var mongoDatabase = app.Services.GetRequiredService<IMongoDatabase>();
await mongoInitializer.InitialiseAsync(mongoDatabase, CancellationToken.None);

var registrationSummary = app.Services.GetRequiredService<AuthorityPluginRegistrationSummary>();
if (registrationSummary.RegisteredPlugins.Count > 0)
{
    app.Logger.LogInformation(
        "Authority plugins registered: {Plugins}",
        string.Join(", ", registrationSummary.RegisteredPlugins));
}

foreach (var failure in registrationSummary.Failures)
{
    app.Logger.LogError(
        "Authority plugin '{PluginName}' failed to register: {Reason}",
        failure.PluginName,
        failure.Reason);
}

foreach (var missing in registrationSummary.MissingOrderedPlugins)
{
    app.Logger.LogWarning(
        "Configured Authority plugin '{PluginName}' was not discovered during startup.",
        missing);
}

var identityProviderRegistry = app.Services.GetRequiredService<IAuthorityIdentityProviderRegistry>();
if (identityProviderRegistry.Providers.Count == 0)
{
    app.Logger.LogWarning("No identity provider plugins were registered.");
}
else
{
    foreach (var provider in identityProviderRegistry.Providers)
    {
        var caps = provider.Capabilities;
        app.Logger.LogInformation(
            "Identity provider plugin '{PluginName}' (type {PluginType}) capabilities: password={Password}, mfa={Mfa}, clientProvisioning={ClientProvisioning}.",
            provider.Name,
            provider.Type,
            caps.SupportsPassword,
            caps.SupportsMfa,
            caps.SupportsClientProvisioning);
    }
}

if (authorityOptions.Bootstrap.Enabled)
{
    var bootstrapGroup = app.MapGroup("/internal");
    bootstrapGroup.AddEndpointFilter(new BootstrapApiKeyFilter(authorityOptions));

    bootstrapGroup.MapPost("/users", async (
        HttpContext httpContext,
        BootstrapUserRequest request,
        IAuthorityIdentityProviderRegistry registry,
        IAuthorityBootstrapInviteStore inviteStore,
        IAuthEventSink auditSink,
        TimeProvider timeProvider,
        CancellationToken cancellationToken) =>
    {
        if (request is null)
        {
            await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, "Request payload is required.", null, null, null, Array.Empty<string>(), null).ConfigureAwait(false);
            return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." });
        }

        var now = timeProvider.GetUtcNow();
        var inviteToken = string.IsNullOrWhiteSpace(request.InviteToken) ? null : request.InviteToken.Trim();
        AuthorityBootstrapInviteDocument? invite = null;
        var inviteReserved = false;

        async Task ReleaseInviteAsync(string reason)
        {
            if (inviteToken is null)
            {
                return;
            }

            if (inviteReserved)
            {
                await inviteStore.ReleaseAsync(inviteToken, cancellationToken).ConfigureAwait(false);
            }

            await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, reason, invite, inviteToken).ConfigureAwait(false);
        }

        if (inviteToken is not null)
        {
            var reservation = await inviteStore.TryReserveAsync(inviteToken, BootstrapInviteTypes.User, now, request.Username, cancellationToken).ConfigureAwait(false);

            switch (reservation.Status)
            {
                case BootstrapInviteReservationStatus.Reserved:
                    inviteReserved = true;
                    invite = reservation.Invite;
                    break;
                case BootstrapInviteReservationStatus.Expired:
                    await WriteInviteAuditAsync("authority.bootstrap.invite.expired", AuthEventOutcome.Failure, "Invite expired before use.", reservation.Invite, inviteToken).ConfigureAwait(false);
                    return Results.BadRequest(new { error = "invite_expired", message = "Invite has expired." });
                case BootstrapInviteReservationStatus.AlreadyUsed:
                    await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, "Invite token already consumed.", reservation.Invite, inviteToken).ConfigureAwait(false);
                    return Results.BadRequest(new { error = "invite_used", message = "Invite token has already been used." });
                default:
                    await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, "Invite token not found.", reservation.Invite, inviteToken).ConfigureAwait(false);
                    return Results.BadRequest(new { error = "invalid_invite", message = "Invite token is invalid." });
            }
        }

        var providerName = string.IsNullOrWhiteSpace(request.Provider)
            ? invite?.Provider ?? authorityOptions.Bootstrap.DefaultIdentityProvider
            : request.Provider;

        if (invite is not null && !string.IsNullOrWhiteSpace(invite.Provider) &&
            !string.Equals(invite.Provider, providerName, StringComparison.OrdinalIgnoreCase))
        {
            await ReleaseInviteAsync("Invite provider does not match requested provider.");
            return Results.BadRequest(new { error = "invite_provider_mismatch", message = "Invite is limited to a different identity provider." });
        }

        if (string.IsNullOrWhiteSpace(providerName) || !registry.TryGet(providerName!, out var provider))
        {
            await ReleaseInviteAsync("Specified identity provider was not found.");
            await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, "Specified identity provider was not found.", null, request.Username, providerName, request.Roles ?? Array.Empty<string>(), inviteToken).ConfigureAwait(false);
            return Results.BadRequest(new { error = "invalid_provider", message = "Specified identity provider was not found." });
        }

        if (!provider.Capabilities.SupportsPassword)
        {
            await ReleaseInviteAsync("Selected provider does not support password provisioning.");
            await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, "Selected provider does not support password provisioning.", null, request.Username, provider.Name, request.Roles ?? Array.Empty<string>(), inviteToken).ConfigureAwait(false);
            return Results.BadRequest(new { error = "unsupported_provider", message = "Selected provider does not support password provisioning." });
        }

        if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrEmpty(request.Password))
        {
            await ReleaseInviteAsync("Username and password are required.");
            await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, "Username and password are required.", null, request.Username, provider.Name, request.Roles ?? Array.Empty<string>(), inviteToken).ConfigureAwait(false);
            return Results.BadRequest(new { error = "invalid_request", message = "Username and password are required." });
        }

        if (invite is not null && !string.IsNullOrWhiteSpace(invite.Target) &&
            !string.Equals(invite.Target, request.Username, StringComparison.OrdinalIgnoreCase))
        {
            await ReleaseInviteAsync("Invite target does not match requested username.");
            return Results.BadRequest(new { error = "invite_target_mismatch", message = "Invite target does not match username." });
        }

        var roles = request.Roles is null ? Array.Empty<string>() : request.Roles.ToArray();
        var attributes = request.Attributes is null
            ? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
            : new Dictionary<string, string?>(request.Attributes, StringComparer.OrdinalIgnoreCase);

        var registration = new AuthorityUserRegistration(
            request.Username,
            request.Password,
            request.DisplayName,
            request.Email,
            request.RequirePasswordReset,
            roles,
            attributes);

        try
        {
            var result = await provider.Credentials.UpsertUserAsync(registration, cancellationToken).ConfigureAwait(false);

            if (!result.Succeeded || result.Value is null)
            {
                await ReleaseInviteAsync(result.Message ?? "User provisioning failed.");
                await WriteBootstrapUserAuditAsync(AuthEventOutcome.Failure, result.Message ?? "User provisioning failed.", null, request.Username, provider.Name, roles, inviteToken).ConfigureAwait(false);
                return Results.BadRequest(new { error = result.ErrorCode ?? "bootstrap_failed", message = result.Message ?? "User provisioning failed." });
            }

            if (inviteReserved && inviteToken is not null)
            {
                var consumed = await inviteStore.MarkConsumedAsync(inviteToken, result.Value.SubjectId ?? result.Value.Username, now, cancellationToken).ConfigureAwait(false);
                if (consumed)
                {
                    await WriteInviteAuditAsync("authority.bootstrap.invite.consumed", AuthEventOutcome.Success, null, invite, inviteToken).ConfigureAwait(false);
                }
            }

            await WriteBootstrapUserAuditAsync(AuthEventOutcome.Success, null, result.Value.SubjectId, result.Value.Username, provider.Name, roles, inviteToken).ConfigureAwait(false);

            return Results.Ok(new
            {
                provider = provider.Name,
                subjectId = result.Value.SubjectId,
                username = result.Value.Username
            });
        }
        catch
        {
            if (inviteReserved && inviteToken is not null)
            {
                await inviteStore.ReleaseAsync(inviteToken, cancellationToken).ConfigureAwait(false);
                await WriteInviteAuditAsync("authority.bootstrap.invite.released", AuthEventOutcome.Error, "Invite released due to provisioning failure.", invite, inviteToken).ConfigureAwait(false);
            }

            throw;
        }

        async Task WriteBootstrapUserAuditAsync(AuthEventOutcome outcome, string? reason, string? subjectId, string? usernameValue, string? providerValue, IReadOnlyCollection<string> rolesValue, string? inviteValue)
        {
            var correlationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
            AuthEventNetwork? network = null;
            var remoteAddress = httpContext.Connection.RemoteIpAddress?.ToString();
            var userAgent = httpContext.Request.Headers.UserAgent.ToString();

            if (!string.IsNullOrWhiteSpace(remoteAddress) || !string.IsNullOrWhiteSpace(userAgent))
            {
                network = new AuthEventNetwork
                {
                    RemoteAddress = ClassifiedString.Personal(remoteAddress),
                    UserAgent = ClassifiedString.Personal(string.IsNullOrWhiteSpace(userAgent) ? null : userAgent)
                };
            }

            var subject = subjectId is null && string.IsNullOrWhiteSpace(usernameValue) && string.IsNullOrWhiteSpace(providerValue)
                ? null
                : new AuthEventSubject
                {
                    SubjectId = ClassifiedString.Personal(subjectId),
                    Username = ClassifiedString.Personal(usernameValue),
                    Realm = ClassifiedString.Public(providerValue)
                };

            var properties = new List<AuthEventProperty>();
            if (!string.IsNullOrWhiteSpace(providerValue))
            {
                properties.Add(new AuthEventProperty
                {
                    Name = "bootstrap.provider",
                    Value = ClassifiedString.Public(providerValue)
                });
            }

            if (!string.IsNullOrWhiteSpace(inviteValue))
            {
                properties.Add(new AuthEventProperty
                {
                    Name = "bootstrap.invite_token",
                    Value = ClassifiedString.Public(inviteValue)
                });
            }

            var scopes = rolesValue is { Count: > 0 }
                ? rolesValue.ToArray()
                : Array.Empty<string>();

            var record = new AuthEventRecord
            {
                EventType = "authority.bootstrap.user",
                OccurredAt = timeProvider.GetUtcNow(),
                CorrelationId = correlationId,
                Outcome = outcome,
                Reason = reason,
                Subject = subject,
                Client = null,
                Scopes = scopes,
                Network = network,
                Properties = properties.Count == 0 ? Array.Empty<AuthEventProperty>() : properties
            };

            await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false);
        }

        async Task WriteInviteAuditAsync(string eventType, AuthEventOutcome outcome, string? reason, AuthorityBootstrapInviteDocument? document, string? tokenValue)
        {
            var record = new AuthEventRecord
            {
                EventType = eventType,
                OccurredAt = timeProvider.GetUtcNow(),
                CorrelationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
                Outcome = outcome,
                Reason = reason,
                Subject = null,
                Client = null,
                Scopes = Array.Empty<string>(),
                Network = null,
                Properties = BuildInviteProperties(document, tokenValue)
            };

            await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false);
        }

        static AuthEventProperty[] BuildInviteProperties(AuthorityBootstrapInviteDocument? document, string? token)
        {
            var properties = new List<AuthEventProperty>();
            if (!string.IsNullOrWhiteSpace(token))
            {
                properties.Add(new AuthEventProperty
                {
                    Name = "invite.token",
                    Value = ClassifiedString.Public(token)
                });
            }

            if (document is not null)
            {
                if (!string.IsNullOrWhiteSpace(document.Type))
                {
                    properties.Add(new AuthEventProperty
                    {
                        Name = "invite.type",
                        Value = ClassifiedString.Public(document.Type)
                    });
                }

                if (!string.IsNullOrWhiteSpace(document.Provider))
                {
                    properties.Add(new AuthEventProperty
                    {
                        Name = "invite.provider",
                        Value = ClassifiedString.Public(document.Provider)
                    });
                }

                if (!string.IsNullOrWhiteSpace(document.Target))
                {
                    properties.Add(new AuthEventProperty
                    {
                        Name = "invite.target",
                        Value = ClassifiedString.Public(document.Target)
                    });
                }

                properties.Add(new AuthEventProperty
                {
                    Name = "invite.expires_at",
                    Value = ClassifiedString.Public(document.ExpiresAt.ToString("O", CultureInfo.InvariantCulture))
                });
            }

            return properties.Count == 0 ? Array.Empty<AuthEventProperty>() : properties.ToArray();
        }
    });

    bootstrapGroup.MapPost("/clients", async (
        HttpContext httpContext,
        BootstrapClientRequest request,
        IAuthorityIdentityProviderRegistry registry,
        IAuthorityBootstrapInviteStore inviteStore,
        IAuthEventSink auditSink,
        TimeProvider timeProvider,
        CancellationToken cancellationToken) =>
    {
        if (request is null)
        {
            await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Request payload is required.", null, null, null, Array.Empty<string>(), null, null).ConfigureAwait(false);
            return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." });
        }

        var now = timeProvider.GetUtcNow();
        var inviteToken = string.IsNullOrWhiteSpace(request.InviteToken) ? null : request.InviteToken.Trim();
        AuthorityBootstrapInviteDocument? invite = null;
        var inviteReserved = false;

        async Task ReleaseInviteAsync(string reason)
        {
            if (inviteToken is null)
            {
                return;
            }

            if (inviteReserved)
            {
                await inviteStore.ReleaseAsync(inviteToken, cancellationToken).ConfigureAwait(false);
            }

            await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, reason, invite, inviteToken).ConfigureAwait(false);
        }

        if (inviteToken is not null)
        {
            var reservation = await inviteStore.TryReserveAsync(inviteToken, BootstrapInviteTypes.Client, now, request.ClientId, cancellationToken).ConfigureAwait(false);
            switch (reservation.Status)
            {
                case BootstrapInviteReservationStatus.Reserved:
                    inviteReserved = true;
                    invite = reservation.Invite;
                    break;
                case BootstrapInviteReservationStatus.Expired:
                    await WriteInviteAuditAsync("authority.bootstrap.invite.expired", AuthEventOutcome.Failure, "Invite expired before use.", reservation.Invite, inviteToken).ConfigureAwait(false);
                    return Results.BadRequest(new { error = "invite_expired", message = "Invite has expired." });
                case BootstrapInviteReservationStatus.AlreadyUsed:
                    await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, "Invite token already consumed.", reservation.Invite, inviteToken).ConfigureAwait(false);
                    return Results.BadRequest(new { error = "invite_used", message = "Invite token has already been used." });
                default:
                    await WriteInviteAuditAsync("authority.bootstrap.invite.rejected", AuthEventOutcome.Failure, "Invite token is invalid.", reservation.Invite, inviteToken).ConfigureAwait(false);
                    return Results.BadRequest(new { error = "invalid_invite", message = "Invite token is invalid." });
            }
        }

        var providerName = string.IsNullOrWhiteSpace(request.Provider)
            ? invite?.Provider ?? authorityOptions.Bootstrap.DefaultIdentityProvider
            : request.Provider;

        if (invite is not null && !string.IsNullOrWhiteSpace(invite.Provider) &&
            !string.Equals(invite.Provider, providerName, StringComparison.OrdinalIgnoreCase))
        {
            await ReleaseInviteAsync("Invite provider does not match requested provider.");
            return Results.BadRequest(new { error = "invite_provider_mismatch", message = "Invite is limited to a different identity provider." });
        }

        if (string.IsNullOrWhiteSpace(providerName) || !registry.TryGet(providerName!, out var provider))
        {
            await ReleaseInviteAsync("Specified identity provider was not found.");
            await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Specified identity provider was not found.", request.ClientId, null, providerName, request.AllowedScopes ?? Array.Empty<string>(), request?.Confidential, inviteToken).ConfigureAwait(false);
            return Results.BadRequest(new { error = "invalid_provider", message = "Specified identity provider was not found." });
        }

        if (!provider.Capabilities.SupportsClientProvisioning || provider.ClientProvisioning is null)
        {
            await ReleaseInviteAsync("Selected provider does not support client provisioning.");
            await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Selected provider does not support client provisioning.", request.ClientId, null, provider.Name, request.AllowedScopes ?? Array.Empty<string>(), request.Confidential, inviteToken).ConfigureAwait(false);
            return Results.BadRequest(new { error = "unsupported_provider", message = "Selected provider does not support client provisioning." });
        }

        if (string.IsNullOrWhiteSpace(request.ClientId))
        {
            await ReleaseInviteAsync("ClientId is required.");
            await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "ClientId is required.", null, null, provider.Name, request.AllowedScopes ?? Array.Empty<string>(), request.Confidential, inviteToken).ConfigureAwait(false);
            return Results.BadRequest(new { error = "invalid_request", message = "ClientId is required." });
        }

        if (invite is not null && !string.IsNullOrWhiteSpace(invite.Target) &&
            !string.Equals(invite.Target, request.ClientId, StringComparison.OrdinalIgnoreCase))
        {
            await ReleaseInviteAsync("Invite target does not match requested client id.");
            return Results.BadRequest(new { error = "invite_target_mismatch", message = "Invite target does not match client id." });
        }

        if (request.Confidential && string.IsNullOrWhiteSpace(request.ClientSecret))
        {
            await ReleaseInviteAsync("Confidential clients require a client secret.");
            await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, "Confidential clients require a client secret.", request.ClientId, null, provider.Name, request.AllowedScopes ?? Array.Empty<string>(), request.Confidential, inviteToken).ConfigureAwait(false);
            return Results.BadRequest(new { error = "invalid_request", message = "Confidential clients require a client secret." });
        }

        if (!TryParseUris(request.RedirectUris, out var redirectUris, out var redirectError))
        {
            var errorMessage = redirectError ?? "Redirect URI validation failed.";
            await ReleaseInviteAsync(errorMessage);
            await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, errorMessage, request.ClientId, null, provider.Name, request.AllowedScopes ?? Array.Empty<string>(), request.Confidential, inviteToken).ConfigureAwait(false);
            return Results.BadRequest(new { error = "invalid_request", message = errorMessage });
        }

        if (!TryParseUris(request.PostLogoutRedirectUris, out var postLogoutUris, out var postLogoutError))
        {
            var errorMessage = postLogoutError ?? "Post-logout redirect URI validation failed.";
            await ReleaseInviteAsync(errorMessage);
            await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, errorMessage, request.ClientId, null, provider.Name, request.AllowedScopes ?? Array.Empty<string>(), request.Confidential, inviteToken).ConfigureAwait(false);
            return Results.BadRequest(new { error = "invalid_request", message = errorMessage });
        }

        var properties = request.Properties is null
            ? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
            : new Dictionary<string, string?>(request.Properties, StringComparer.OrdinalIgnoreCase);

        var registration = new AuthorityClientRegistration(
            request.ClientId,
            request.Confidential,
            request.DisplayName,
            request.ClientSecret,
            request.AllowedGrantTypes ?? Array.Empty<string>(),
            request.AllowedScopes ?? Array.Empty<string>(),
            redirectUris,
            postLogoutUris,
            properties);

        var result = await provider.ClientProvisioning.CreateOrUpdateAsync(registration, cancellationToken).ConfigureAwait(false);

        if (!result.Succeeded || result.Value is null)
        {
            await ReleaseInviteAsync(result.Message ?? "Client provisioning failed.");
            await WriteBootstrapClientAuditAsync(AuthEventOutcome.Failure, result.Message ?? "Client provisioning failed.", request.ClientId, result.Value?.ClientId, provider.Name, request.AllowedScopes ?? Array.Empty<string>(), request.Confidential, inviteToken).ConfigureAwait(false);
            return Results.BadRequest(new { error = result.ErrorCode ?? "bootstrap_failed", message = result.Message ?? "Client provisioning failed." });
        }

        if (inviteReserved && inviteToken is not null)
        {
            var consumed = await inviteStore.MarkConsumedAsync(inviteToken, result.Value.ClientId, now, cancellationToken).ConfigureAwait(false);
            if (consumed)
            {
                await WriteInviteAuditAsync("authority.bootstrap.invite.consumed", AuthEventOutcome.Success, null, invite, inviteToken).ConfigureAwait(false);
            }
        }

        await WriteBootstrapClientAuditAsync(AuthEventOutcome.Success, null, request.ClientId, result.Value.ClientId, provider.Name, request.AllowedScopes ?? Array.Empty<string>(), request.Confidential, inviteToken).ConfigureAwait(false);

        return Results.Ok(new
        {
            provider = provider.Name,
            clientId = result.Value.ClientId,
            confidential = result.Value.Confidential
        });

        async Task WriteBootstrapClientAuditAsync(AuthEventOutcome outcome, string? reason, string? requestedClientId, string? assignedClientId, string? providerValue, IReadOnlyCollection<string> scopes, bool? confidentialFlag, string? inviteValue)
        {
            var correlationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
            AuthEventNetwork? network = null;
            var remoteAddress = httpContext.Connection.RemoteIpAddress?.ToString();
            var userAgent = httpContext.Request.Headers.UserAgent.ToString();

            if (!string.IsNullOrWhiteSpace(remoteAddress) || !string.IsNullOrWhiteSpace(userAgent))
            {
                network = new AuthEventNetwork
                {
                    RemoteAddress = ClassifiedString.Personal(remoteAddress),
                    UserAgent = ClassifiedString.Personal(string.IsNullOrWhiteSpace(userAgent) ? null : userAgent)
                };
            }

            var clientIdValue = assignedClientId ?? requestedClientId;
            var client = clientIdValue is null && string.IsNullOrWhiteSpace(providerValue)
                ? null
                : new AuthEventClient
                {
                    ClientId = ClassifiedString.Personal(clientIdValue),
                    Name = ClassifiedString.Empty,
                    Provider = ClassifiedString.Public(providerValue)
                };

            var properties = new List<AuthEventProperty>();
            if (!string.IsNullOrWhiteSpace(requestedClientId) && !string.Equals(requestedClientId, assignedClientId, StringComparison.Ordinal))
            {
                properties.Add(new AuthEventProperty
                {
                    Name = "bootstrap.requested_client_id",
                    Value = ClassifiedString.Public(requestedClientId)
                });
            }

            if (confidentialFlag == true)
            {
                properties.Add(new AuthEventProperty
                {
                    Name = "bootstrap.confidential",
                    Value = ClassifiedString.Public("true")
                });
            }

            if (!string.IsNullOrWhiteSpace(inviteValue))
            {
                properties.Add(new AuthEventProperty
                {
                    Name = "bootstrap.invite_token",
                    Value = ClassifiedString.Public(inviteValue)
                });
            }

            var record = new AuthEventRecord
            {
                EventType = "authority.bootstrap.client",
                OccurredAt = timeProvider.GetUtcNow(),
                CorrelationId = correlationId,
                Outcome = outcome,
                Reason = reason,
                Subject = null,
                Client = client,
                Scopes = scopes is { Count: > 0 } ? scopes.ToArray() : Array.Empty<string>(),
                Network = network,
                Properties = properties.Count == 0 ? Array.Empty<AuthEventProperty>() : properties.ToArray()
            };

            await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false);
        }

        async Task WriteInviteAuditAsync(string eventType, AuthEventOutcome outcome, string? reason, AuthorityBootstrapInviteDocument? document, string? tokenValue)
        {
            var record = new AuthEventRecord
            {
                EventType = eventType,
                OccurredAt = timeProvider.GetUtcNow(),
                CorrelationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
                Outcome = outcome,
                Reason = reason,
                Subject = null,
                Client = null,
                Scopes = Array.Empty<string>(),
                Network = null,
                Properties = BuildInviteProperties(document, tokenValue)
            };

            await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false);
        }

        static AuthEventProperty[] BuildInviteProperties(AuthorityBootstrapInviteDocument? document, string? token)
        {
            var properties = new List<AuthEventProperty>();
            if (!string.IsNullOrWhiteSpace(token))
            {
                properties.Add(new AuthEventProperty
                {
                    Name = "invite.token",
                    Value = ClassifiedString.Public(token)
                });
            }

            if (document is not null)
            {
                if (!string.IsNullOrWhiteSpace(document.Type))
                {
                    properties.Add(new AuthEventProperty
                    {
                        Name = "invite.type",
                        Value = ClassifiedString.Public(document.Type)
                    });
                }

                if (!string.IsNullOrWhiteSpace(document.Provider))
                {
                    properties.Add(new AuthEventProperty
                    {
                        Name = "invite.provider",
                        Value = ClassifiedString.Public(document.Provider)
                    });
                }

                if (!string.IsNullOrWhiteSpace(document.Target))
                {
                    properties.Add(new AuthEventProperty
                    {
                        Name = "invite.target",
                        Value = ClassifiedString.Public(document.Target)
                    });
                }

                properties.Add(new AuthEventProperty
                {
                    Name = "invite.expires_at",
                    Value = ClassifiedString.Public(document.ExpiresAt.ToString("O", CultureInfo.InvariantCulture))
                });
            }

            return properties.Count == 0 ? Array.Empty<AuthEventProperty>() : properties.ToArray();
        }
    });
    bootstrapGroup.MapPost("/invites", async (
        HttpContext httpContext,
        BootstrapInviteRequest request,
        IAuthorityBootstrapInviteStore inviteStore,
        IAuthEventSink auditSink,
        TimeProvider timeProvider,
        CancellationToken cancellationToken) =>
    {
        if (request is null)
        {
            return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." });
        }

        if (string.IsNullOrWhiteSpace(request.Type) ||
            ( !string.Equals(request.Type, BootstrapInviteTypes.User, StringComparison.OrdinalIgnoreCase) &&
              !string.Equals(request.Type, BootstrapInviteTypes.Client, StringComparison.OrdinalIgnoreCase)))
        {
            return Results.BadRequest(new { error = "invalid_request", message = "Invite type must be 'user' or 'client'." });
        }

        var now = timeProvider.GetUtcNow();
        var expiresAt = request.ExpiresAt ?? now.AddDays(2);
        if (expiresAt <= now)
        {
            return Results.BadRequest(new { error = "invalid_request", message = "ExpiresAt must be in the future." });
        }

        var token = string.IsNullOrWhiteSpace(request.Token) ? Guid.NewGuid().ToString("N") : request.Token.Trim();

        var document = new AuthorityBootstrapInviteDocument
        {
            Token = token,
            Type = request.Type.ToLowerInvariant(),
            Provider = string.IsNullOrWhiteSpace(request.Provider) ? null : request.Provider.Trim(),
            Target = string.IsNullOrWhiteSpace(request.Target) ? null : request.Target.Trim(),
            IssuedAt = now,
            IssuedBy = string.IsNullOrWhiteSpace(request.IssuedBy) ? httpContext.User?.Identity?.Name : request.IssuedBy,
            ExpiresAt = expiresAt,
            Metadata = request.Metadata is null ? null : new Dictionary<string, string?>(request.Metadata, StringComparer.OrdinalIgnoreCase)
        };

        await inviteStore.CreateAsync(document, cancellationToken).ConfigureAwait(false);
        await WriteInviteAuditAsync("authority.bootstrap.invite.created", AuthEventOutcome.Success, null, document).ConfigureAwait(false);

        return Results.Ok(new
        {
            document.Token,
            document.Type,
            document.Provider,
            document.Target,
            document.ExpiresAt
        });

        async Task WriteInviteAuditAsync(string eventType, AuthEventOutcome outcome, string? reason, AuthorityBootstrapInviteDocument invite)
        {
            var record = new AuthEventRecord
            {
                EventType = eventType,
                OccurredAt = timeProvider.GetUtcNow(),
                CorrelationId = Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
                Outcome = outcome,
                Reason = reason,
                Subject = null,
                Client = null,
                Scopes = Array.Empty<string>(),
                Network = null,
                Properties = BuildInviteProperties(invite)
            };

            await auditSink.WriteAsync(record, httpContext.RequestAborted).ConfigureAwait(false);
        }

        static AuthEventProperty[] BuildInviteProperties(AuthorityBootstrapInviteDocument invite)
        {
            var properties = new List<AuthEventProperty>
            {
                new() { Name = "invite.token", Value = ClassifiedString.Public(invite.Token) },
                new() { Name = "invite.type", Value = ClassifiedString.Public(invite.Type) },
                new() { Name = "invite.expires_at", Value = ClassifiedString.Public(invite.ExpiresAt.ToString("O", CultureInfo.InvariantCulture)) }
            };

            if (!string.IsNullOrWhiteSpace(invite.Provider))
            {
                properties.Add(new AuthEventProperty { Name = "invite.provider", Value = ClassifiedString.Public(invite.Provider) });
            }

            if (!string.IsNullOrWhiteSpace(invite.Target))
            {
                properties.Add(new AuthEventProperty { Name = "invite.target", Value = ClassifiedString.Public(invite.Target) });
            }

            if (!string.IsNullOrWhiteSpace(invite.IssuedBy))
            {
                properties.Add(new AuthEventProperty { Name = "invite.issued_by", Value = ClassifiedString.Public(invite.IssuedBy) });
            }

            return properties.ToArray();
        }
    });

    bootstrapGroup.MapGet("/revocations/export", async (
        AuthorityRevocationExportService exportService,
        CancellationToken cancellationToken) =>
    {
        var package = await exportService.ExportAsync(cancellationToken).ConfigureAwait(false);
        var build = package.Bundle;

        var response = new RevocationExportResponse
        {
            SchemaVersion = build.Bundle.SchemaVersion,
            BundleId = build.Bundle.BundleId ?? build.Sha256,
            Sequence = build.Sequence,
            IssuedAt = build.IssuedAt,
            SigningKeyId = package.Signature.KeyId,
            Bundle = new RevocationExportPayload
            {
                Data = Convert.ToBase64String(build.CanonicalJson)
            },
            Signature = new RevocationExportSignature
            {
                Algorithm = package.Signature.Algorithm,
                KeyId = package.Signature.KeyId,
                Provider = package.Signature.Provider,
                Value = package.Signature.Value
            },
            Digest = new RevocationExportDigest
            {
                Value = build.Sha256
            }
        };

        return Results.Ok(response);
    });

    bootstrapGroup.MapPost("/signing/rotate", (
        SigningRotationRequest? request,
        AuthoritySigningKeyManager signingManager,
        ILogger<AuthoritySigningKeyManager> signingLogger) =>
    {
        if (request is null)
        {
            signingLogger.LogWarning("Signing rotation request payload missing.");
            return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." });
        }

        try
        {
            var result = signingManager.Rotate(request);
            signingLogger.LogInformation("Signing key rotation completed. Active key {KeyId}.", result.ActiveKeyId);

            return Results.Ok(new
            {
                activeKeyId = result.ActiveKeyId,
                provider = result.ActiveProvider,
                source = result.ActiveSource,
                location = result.ActiveLocation,
                previousKeyId = result.PreviousKeyId,
                retiredKeyIds = result.RetiredKeyIds
            });
        }
        catch (InvalidOperationException ex)
        {
            signingLogger.LogWarning(ex, "Signing rotation failed due to invalid input.");
            return Results.BadRequest(new { error = "rotation_failed", message = ex.Message });
        }
        catch (Exception ex)
        {
            signingLogger.LogError(ex, "Unexpected failure rotating signing key.");
            return Results.Problem("Failed to rotate signing key.");
        }
    });
}

app.UseSerilogRequestLogging(options =>
{
    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
    {
        diagnosticContext.Set("TraceId", Activity.Current?.TraceId.ToString());
        diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());
    };
});

app.UseExceptionHandler(static errorApp =>
{
    errorApp.Run(async context =>
    {
        context.Response.ContentType = "application/problem+json";
        var problem = Results.Problem(
            statusCode: StatusCodes.Status500InternalServerError,
            title: "Unhandled server error",
            detail: "Unexpected failure while processing the request.");

        await problem.ExecuteAsync(context);
    });
});

app.UseRouting();
app.UseAuthorityRateLimiterContext();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/health", async (IAuthorityIdentityProviderRegistry registry, CancellationToken cancellationToken) =>
    {
        var pluginHealth = new List<object>();
        foreach (var provider in registry.Providers)
        {
            var health = await provider.CheckHealthAsync(cancellationToken).ConfigureAwait(false);
            pluginHealth.Add(new
            {
                provider = provider.Name,
                status = health.Status.ToString().ToLowerInvariant(),
                message = health.Message
            });
        }

        return Results.Ok(new
        {
            status = "healthy",
            identityProviders = pluginHealth
        });
    })
    .WithName("HealthCheck");

app.MapGet("/ready", (IAuthorityIdentityProviderRegistry registry) =>
    Results.Ok(new
    {
        status = registry.Providers.Count > 0 ? "ready" : "degraded",
        identityProviders = registry.Providers.Select(p => p.Name).ToArray()
    }))
    .WithName("ReadinessCheck");

app.MapGet("/jwks", (AuthorityJwksService jwksService) => Results.Ok(jwksService.Build()))
    .WithName("JsonWebKeySet");

// Ensure signing key manager initialises key material on startup.
app.Services.GetRequiredService<AuthoritySigningKeyManager>();

app.Run();

static PluginHostOptions BuildPluginHostOptions(StellaOpsAuthorityOptions options, string basePath)
{
    var pluginDirectory = options.PluginDirectories.FirstOrDefault();
    var hostOptions = new PluginHostOptions
    {
        BaseDirectory = basePath,
        PluginsDirectory = string.IsNullOrWhiteSpace(pluginDirectory)
            ? Path.Combine("PluginBinaries", "Authority")
            : pluginDirectory,
        PrimaryPrefix = "StellaOps.Authority"
    };

    if (!hostOptions.SearchPatterns.Any(pattern => string.Equals(pattern, "StellaOps.Authority.Plugin.*.dll", StringComparison.OrdinalIgnoreCase)))
    {
        hostOptions.SearchPatterns.Add("StellaOps.Authority.Plugin.*.dll");
    }

    foreach (var pair in options.Plugins.Descriptors.OrderBy(static p => p.Key, StringComparer.OrdinalIgnoreCase))
    {
        var descriptor = pair.Value;
        if (descriptor.Enabled && !string.IsNullOrWhiteSpace(descriptor.AssemblyName))
        {
            hostOptions.PluginOrder.Add(descriptor.AssemblyName!);
        }
    }

    return hostOptions;
}

static bool TryParseUris(IReadOnlyCollection<string>? values, out IReadOnlyCollection<Uri> uris, out string? error)
{
    error = null;

    if (values is null || values.Count == 0)
    {
        uris = Array.Empty<Uri>();
        return true;
    }

    var parsed = new List<Uri>(values.Count);
    foreach (var entry in values)
    {
        if (string.IsNullOrWhiteSpace(entry) || !Uri.TryCreate(entry, UriKind.Absolute, out var uri))
        {
            uris = Array.Empty<Uri>();
            error = $"Invalid URI value '{entry}'.";
            return false;
        }

        parsed.Add(uri);
    }

    uris = parsed;
    return true;
}
