using Ablera.Serdica.Common.Tools.Helpers;
using Ablera.Serdica.Microservice.Consumer.Config;
using Ablera.Serdica.Authority.Services;
using Ablera.Serdica.DBModels.Serdica;
using Serilog;
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using System;
using Ablera.Serdica.Extensions.Serilog;
using Microsoft.EntityFrameworkCore;
using Ablera.Serdica.DBModels.Oidc;
using Microsoft.AspNetCore.Builder;
using Ablera.Serdica.Authority.HostServices;
using Microsoft.IdentityModel.Tokens;
using Quartz;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.Cookies;
using Ablera.Serdica.Authority.Extensions;
using Ablera.Serdica.Authority.Models;
using Ablera.Serdica.Authority.Contracts;
using Microsoft.AspNetCore.Identity;
using Ablera.Serdica.Common.Tools.Models.Config;
using Microsoft.IdentityModel.Protocols.Configuration;
using Microsoft.Extensions.Options;
using Ablera.Serdica.Authority.Constants;
using OpenIddict.Server;
using Ablera.Serdica.Authority.OpenIddictServerHandlers;
using Ablera.Serdica.DependencyInjection;
using Ablera.Serdica.UserConfiguration.Models;
using Ablera.Serdica.HealthChecks.Extensions;
using Microsoft.AspNetCore.StaticFiles;

using static OpenIddict.Server.OpenIddictServerEvents;

// Use the W3C format for Activity IDs.
Activity.DefaultIdFormat = ActivityIdFormat.W3C;

// Create the WebApplicationBuilder instead of using Host.CreateDefaultBuilder.
// This model ensures that the built application supports middleware configuration.
var builder = WebApplication.CreateBuilder(args);

// Adjust configuration – set the environment name (if provided by an environment variable)
// and add the "SERDICA_" prefixed environment variables.
{
    var environmentName = Environment.GetEnvironmentVariable("SERDICA_PROJECT_ENV")
                          ?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
    if (!string.IsNullOrWhiteSpace(environmentName))
    {
        builder.Environment.EnvironmentName = environmentName;
    }
    builder.Configuration.AddEnvironmentVariables(prefix: "SERDICA_");
}

// Configure Serilog as the logging provider.
builder.Host.UseSerilog((context, _, configuration) =>
{
    configuration.ReadFrom.Configuration(context.Configuration)
                 .Enrich.With(new MoveScopeToFieldsLogEventEnricher());
});
var jsonSettingsConfig = builder.Configuration.GetSection(nameof(JsonFileSettingsConfig)).Get<JsonFileSettingsConfig>() ?? new JsonFileSettingsConfig();
using var oidConfigProvider = new OidcJsonSettingsProvider(null, Options.Create(jsonSettingsConfig));
var oidcSettings = oidConfigProvider.Settings;

var issuerUrl = oidcSettings.IssuerUrl.TrimEnd('/');

var oidcEncryptionKey = Convert.FromBase64String(oidcSettings.EncryptionKey!)
    ?? throw new InvalidConfigurationException($"Invalid or no base64 key provided for {nameof(OidcServerSettings)}.{nameof(OidcServerSettings.EncryptionKey)}");

// Register Ablera Serdica configuration.
builder.Services
    .ConfigureTools(builder.Configuration)
    .AddRedisFromEntityFrameworkEntityCacheManager<Route, SerdicaDbContext>(
        builder.Configuration, e => e.Id.ToString())
    .AddDbContext<SerdicaDbContext>(
        builder.Configuration, OptimizedSerdicaDbContextModel.Instance)
    .AddDbContext<OidcDbContext>(
        builder.Configuration, null, null, options =>
            options
                .UseOracle(
                    builder.Configuration.GetConnectionString(ConstantsClass.ConnectionNameDefault),
                    b => b.MigrationsAssembly(typeof(Ablera.Serdica.DBModels.Oidc.Migrations.OidcDbContextFactory).Assembly.GetName().Name))
                .UseOpenIddict())
    .AddInitializationRoutine<Ablera.Serdica.Microservice.Initializer.EndpointsRegistration.Initializer>()
    .AddTranslationProvider(builder.Configuration)
    .AddCacheManager(builder.Configuration)
    .AddUserConfiguration<UserConfigurationModel>(builder.Configuration)
    .AddRedis(builder.Configuration)
    .AddRedisUserConfigurationRepository<UserConfigurationModel>(builder.Configuration)
    .AddSerdicaUserConfigurationBuilder(builder.Configuration)
    .AddPluginIntegrations(builder.Configuration)
    .AddSystem(builder.Configuration)
    .AddAsConsumerAsync(builder.Configuration);

// Register Ablera.Serdica.Authority services
builder.Services
    .Configure<UserManagingDirectorConfig>(builder.Configuration.GetSection(nameof(UserManagingDirectorConfig)))
    .Configure<FileServerConfig>(builder.Configuration.GetSection(nameof(FileServerConfig)))
    .Configure<OidcServerSettings>(builder.Configuration.GetSection(nameof(OidcServerSettings)))
    .AddSingleton<RoutesTreeProvider>()
    .AddSingleton<OidcJsonSettingsProvider>()
    .AddScoped<OidcClientSynchronizer>()
    .AddSingleton<AuthenticationUrlBuilder>()
    .AddScoped<IUserManagingDirector<IdentityUser<string>>, UserManagingDirector>()
    .AddHostedService<RoutesTreeBuilderHostedService>()
    .AddHostedService<OidcInfrastructureHostedService>();

// Get FileServerConfig to determine the correct paths with prefix
var fileServerConfig = builder.Configuration.GetSection(nameof(FileServerConfig)).Get<FileServerConfig>() ?? new FileServerConfig();
var pathPrefix = fileServerConfig.RootPathPrefixForWWW ?? string.Empty;

// Configure authentication using cookies.
builder
    .Services
    .AddSession(options =>
    {
        options.IdleTimeout = TimeSpan.FromMinutes(1);
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.None;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    })
    .AddHttpContextAccessor()
    .AddCors(options =>
    {
        options
            .AddDefaultPolicy(policy => policy
            .SetIsOriginAllowed(_ => true) // Allow any origin
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials()
            .AllowCredentials());
    })
    .AddQuartz(options =>
    {
        options.UseSimpleTypeLoader();
        options.UseInMemoryStore();
    })
    .AddQuartzHostedService(options => options.WaitForJobsToComplete = true)
    .AddAuthorization()
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        var loginPath = string.IsNullOrEmpty(pathPrefix) 
            ? "/login.html" 
            : $"{pathPrefix}/login.html";
        
        var accessDeniedPath = string.IsNullOrEmpty(pathPrefix)
            ? oidcSettings.Endpoints.Authorization.EnsureStartsWith("/")
            : $"{pathPrefix}{oidcSettings.Endpoints.Authorization.EnsureStartsWith("/")}";

        var logoutPath = string.IsNullOrEmpty(pathPrefix)
            ? oidcSettings.Endpoints.Logout.EnsureStartsWith("/")
            : $"{pathPrefix}{oidcSettings.Endpoints.Logout.EnsureStartsWith("/")}";

        options.AccessDeniedPath = accessDeniedPath;
        options.LoginPath = loginPath;
        options.LogoutPath = logoutPath;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.Cookie.SecurePolicy = CookieSecurePolicy.None;
        options.Cookie.Name = oidcSettings.CookieName;
        options.SlidingExpiration = true;
        options.ExpireTimeSpan = TimeSpan.FromMinutes(oidcSettings.CookieExpirationInMinutes);
        options.Events = new CookieAuthenticationEvents
        {
            OnRedirectToLogin = x => x.HandlerRedirectToLogin()
        };
    }).Services
    .AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>, ConfigureCookieTicketStore>()
    .AddSingleton<ITicketStore, RedisTicketStore>()
    .AddRedis(builder.Configuration);

// Register health checks
builder.Services
    .AddHealthChecks(builder.Configuration, typeof(SerdicaDbContext), typeof(OidcDbContext))
    .AddRedis(builder.Configuration)
    .AddRabbitMQ(builder.Configuration, builder.Services);

builder.Services
    .AddDataProtection(builder.Configuration);

// Register OpenIddict.
builder.Services.AddOpenIddict()
    .AddCore(options =>
    {
        // Use your Oracle-based SerdicaDbContext for OpenIddict's stores.
        options.UseEntityFrameworkCore()
                .UseDbContext<OidcDbContext>();
        options.UseQuartz();
    })
    .AddServer(options =>
    {
        options.SetIssuer(new Uri(issuerUrl));

        options.SetAuthorizationCodeLifetime(TimeSpan.FromMinutes(oidcSettings.AuthorizationTokenDurationInMinutes));
        
        // Get FileServerConfig to apply path prefix to endpoints
        var fileServerConfig = builder.Configuration.GetSection(nameof(FileServerConfig)).Get<FileServerConfig>() ?? new FileServerConfig();
        var pathPrefix = fileServerConfig.RootPathPrefixForWWW ?? string.Empty;
        
        options
            .SetAuthorizationEndpointUris(
                $"{pathPrefix}{oidcSettings.Endpoints.Authorization.EnsureStartsWith("/")}")
            .SetDeviceAuthorizationEndpointUris(
                $"{pathPrefix}{oidcSettings.Endpoints.Device.EnsureStartsWith("/")}")
            .SetIntrospectionEndpointUris(
                $"{pathPrefix}{oidcSettings.Endpoints.Introspection.EnsureStartsWith("/")}")
            .SetEndSessionEndpointUris(
                $"{pathPrefix}{oidcSettings.Endpoints.Logout.EnsureStartsWith("/")}")
            .SetTokenEndpointUris(
                $"{pathPrefix}{oidcSettings.Endpoints.Token.EnsureStartsWith("/")}")
            .SetUserInfoEndpointUris(
                $"{pathPrefix}{oidcSettings.Endpoints.Userinfo.EnsureStartsWith("/")}")
            .SetRevocationEndpointUris(
                $"{pathPrefix}{oidcSettings.Endpoints.Revocation.EnsureStartsWith("/")}")
            .SetEndUserVerificationEndpointUris(
                $"{pathPrefix}{oidcSettings.Endpoints.EndUserVerification.EnsureStartsWith("/")}")
            .SetJsonWebKeySetEndpointUris(
                $"{pathPrefix}{oidcSettings.Endpoints.Jwks.EnsureStartsWith("/")}")
            .SetConfigurationEndpointUris(
                $"{pathPrefix}{oidcSettings.Endpoints.Configuration.EnsureStartsWith("/")}");

        options
            .AllowAuthorizationCodeFlow()
            .AllowHybridFlow()
            .AllowClientCredentialsFlow()
            .AcceptAnonymousClients()
            .AllowPasswordFlow()
            .AllowRefreshTokenFlow()
            .AllowDeviceAuthorizationFlow()
            .AllowNoneFlow();

        options.AddEventHandler<HandleAuthorizationRequestContext>(
            x => x
                .UseScopedHandler<AuthorizationRequestHandler>()
                .SetOrder(int.MinValue)
                //.SetOrder(OpenIddictServerHandlers.Authentication.ValidateAuthentication.Descriptor.Order + 1)
                .SetType(OpenIddictServerHandlerType.Custom));
        options.AddEventHandler<ValidateTokenRequestContext>(x => x
            .UseScopedHandler<ValidateClientCredentialsRequest>()
            .SetType(OpenIddictServerHandlerType.Custom));
        options.AddEventHandler<HandleTokenRequestContext>(
            x => x
                .UseScopedHandler<ClientCredentialsGrantHandler>()
                .SetOrder(OpenIddictServerHandlers.ValidateIdentityToken.Descriptor.Order + 1)
                .SetType(OpenIddictServerHandlerType.Custom));
        options.AddEventHandler<HandleTokenRequestContext>(
            x => x
                .UseScopedHandler<PasswordGrantHandler>()
                .SetOrder(OpenIddictServerHandlers.ValidateIdentityToken.Descriptor.Order + 2)
                .SetType(OpenIddictServerHandlerType.Custom));
        options.AddEventHandler<HandleEndSessionRequestContext>(
            x => x.UseScopedHandler<EndSessionHandler>());

        options.RegisterClaims(oidcSettings.Claims);
        options.RegisterScopes(oidcSettings.Scopes);

        options.RequireProofKeyForCodeExchange();

        // Use development certificates – replace with a production certificate in real applications.
        options.AddDevelopmentEncryptionCertificate()
                .AddDevelopmentSigningCertificate();

        options.UseDataProtection()
            .PreferDefaultAccessTokenFormat();

        options.AddEncryptionKey(new SymmetricSecurityKey(oidcEncryptionKey));
        var aspNetCoreConfiguration = options.UseAspNetCore();
        aspNetCoreConfiguration.EnableStatusCodePagesIntegration();
        if (oidcSettings.RequireHttps != true)
        {
            aspNetCoreConfiguration.DisableTransportSecurityRequirement();
        }
    })
    .AddValidation(options =>
    {
        options.UseLocalServer();
        options.AddEncryptionKey(new SymmetricSecurityKey(oidcEncryptionKey));
        options.UseSystemNetHttp();
        options.UseAspNetCore();
        options.UseDataProtection();
        options.EnableAuthorizationEntryValidation();
    });

// Build the WebApplication.
var app = builder.Build();

// Configure the middleware pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app
    .UseForwardedHeadersExt(builder.Configuration)
    .UseRouting();

// Configure static files with path prefix support
if (!string.IsNullOrEmpty(fileServerConfig.RootPathPrefixForWWW))
{
    // Serve static files with path prefix (e.g., /identity)
    app.UseStaticFiles(new StaticFileOptions
    {
        RequestPath = fileServerConfig.RootPathPrefixForWWW
    });
}
else
{
    // Serve static files at root level (default behavior)
    app.UseStaticFiles();
}

app
    .UseCors()
    .UseAuthentication()
    .UseAuthorization();

app.MapHealthChecks();

// Bind the service provider if needed (legacy support).
ServiceProviderAccessor.Initialize(app.Services);

// Start the application within a try/catch to log errors.
try
{
    Log.Information("Starting application with issuer url {issuerUrl}", issuerUrl);

    ServiceProviderAccessor.Initialize(app.Services);

    await app.RunAsync();

    return 0;
}
catch (Exception ex)
{
    Log.Fatal(ex, "Host terminated unexpectedly.");
    Console.WriteLine("Host terminated unexpectedly. " +
        "Exception: " + ex.Message + Environment.NewLine +
        "Stacktrace: " + ex.StackTrace);
    return 1;
}
finally
{
    Log.CloseAndFlush();
}

