using System.Diagnostics;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
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.Plugins.Abstractions;
using StellaOps.Authority.Plugins;
using StellaOps.Authority.Bootstrap;
using StellaOps.Authority.Storage.Mongo.Extensions;
using StellaOps.Authority.Storage.Mongo.Initialization;
using StellaOps.Configuration;
using StellaOps.Plugin.DependencyInjection;
using StellaOps.Plugin.Hosting;
using StellaOps.Authority.OpenIddict.Handlers;
using System.Linq;

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));

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.AddScoped<ValidatePasswordGrantHandler>();
builder.Services.AddScoped<HandlePasswordGrantHandler>();
builder.Services.AddScoped<ValidateClientCredentialsHandler>();
builder.Services.AddScoped<HandleClientCredentialsHandler>();
builder.Services.AddScoped<ValidateAccessTokenHandler>();

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>();
        });
    });

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 (
        BootstrapUserRequest request,
        IAuthorityIdentityProviderRegistry registry,
        CancellationToken cancellationToken) =>
    {
        if (request is null)
        {
            return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." });
        }

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

        if (string.IsNullOrWhiteSpace(providerName) || !registry.TryGet(providerName!, out var provider))
        {
            return Results.BadRequest(new { error = "invalid_provider", message = "Specified identity provider was not found." });
        }

        if (!provider.Capabilities.SupportsPassword)
        {
            return Results.BadRequest(new { error = "unsupported_provider", message = "Selected provider does not support password provisioning." });
        }

        if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrEmpty(request.Password))
        {
            return Results.BadRequest(new { error = "invalid_request", message = "Username and password are required." });
        }

        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);

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

        if (!result.Succeeded || result.Value is null)
        {
            return Results.BadRequest(new { error = result.ErrorCode ?? "bootstrap_failed", message = result.Message ?? "User provisioning failed." });
        }

        return Results.Ok(new
        {
            provider = provider.Name,
            subjectId = result.Value.SubjectId,
            username = result.Value.Username
        });
    });

    bootstrapGroup.MapPost("/clients", async (
        BootstrapClientRequest request,
        IAuthorityIdentityProviderRegistry registry,
        CancellationToken cancellationToken) =>
    {
        if (request is null)
        {
            return Results.BadRequest(new { error = "invalid_request", message = "Request payload is required." });
        }

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

        if (string.IsNullOrWhiteSpace(providerName) || !registry.TryGet(providerName!, out var provider))
        {
            return Results.BadRequest(new { error = "invalid_provider", message = "Specified identity provider was not found." });
        }

        if (!provider.Capabilities.SupportsClientProvisioning || provider.ClientProvisioning is null)
        {
            return Results.BadRequest(new { error = "unsupported_provider", message = "Selected provider does not support client provisioning." });
        }

        if (string.IsNullOrWhiteSpace(request.ClientId))
        {
            return Results.BadRequest(new { error = "invalid_request", message = "ClientId is required." });
        }

        if (request.Confidential && string.IsNullOrWhiteSpace(request.ClientSecret))
        {
            return Results.BadRequest(new { error = "invalid_request", message = "Confidential clients require a client secret." });
        }

        if (!TryParseUris(request.RedirectUris, out var redirectUris, out var redirectError))
        {
            return Results.BadRequest(new { error = "invalid_request", message = redirectError });
        }

        if (!TryParseUris(request.PostLogoutRedirectUris, out var postLogoutUris, out var postLogoutError))
        {
            return Results.BadRequest(new { error = "invalid_request", message = postLogoutError });
        }

        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)
        {
            return Results.BadRequest(new { error = result.ErrorCode ?? "bootstrap_failed", message = result.Message ?? "Client provisioning failed." });
        }

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

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.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.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;
}
