using System.Text;
using System.Net;
using System.Text.Json.Serialization;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Domain.System;
using Jiaowu.Api.Infrastructure.BackgroundJobs;
using Jiaowu.Api.Infrastructure.Grades;
using Jiaowu.Api.Infrastructure.Configuration;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Analytics;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Exams;
using Jiaowu.Api.Infrastructure.Middleware;
using Jiaowu.Api.Infrastructure.Observability;
using Jiaowu.Api.Infrastructure.OfficialDocuments;
using Jiaowu.Api.Infrastructure.Operations;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Scheduling;
using Jiaowu.Api.Infrastructure.Timetables;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerUI;
using System.Threading.RateLimiting;

EnvironmentFile.Load();

var migrateOnly = args.Contains("--migrate-only", StringComparer.OrdinalIgnoreCase);
var seedDemoData = args.Contains("--seed-demo-data", StringComparer.OrdinalIgnoreCase);
var confirmProductionDemoData = args.Contains(
    "--confirm-production-demo-data",
    StringComparer.OrdinalIgnoreCase);

if (migrateOnly && seedDemoData)
{
    Console.Error.WriteLine(
        "--migrate-only 与 --seed-demo-data 不能同时使用。请先迁移，再单独插入演示数据。");
    Environment.ExitCode = 2;
    return;
}

if (seedDemoData && !confirmProductionDemoData)
{
    Console.Error.WriteLine(
        "插入生产演示数据必须同时传入 --confirm-production-demo-data。");
    Environment.ExitCode = 2;
    return;
}

if (confirmProductionDemoData && !seedDemoData)
{
    Console.Error.WriteLine(
        "--confirm-production-demo-data 只能与 --seed-demo-data 同时使用。");
    Environment.ExitCode = 2;
    return;
}

var builder = WebApplication.CreateBuilder(args);
var swaggerDocumentVersion = typeof(Program).Assembly
    .GetCustomAttributes(
        typeof(System.Reflection.AssemblyMetadataAttribute),
        inherit: false)
    .OfType<System.Reflection.AssemblyMetadataAttribute>()
    .SingleOrDefault(x => x.Key == "SwaggerDocumentVersion")?.Value
    ?? "v1";

if (seedDemoData && builder.Environment.IsDevelopment())
{
    Console.Error.WriteLine(
        "--seed-demo-data 仅用于独立的非 Development 演示环境数据库。");
    Environment.ExitCode = 2;
    return;
}

var databaseOptions = builder.Configuration
    .GetSection(DatabaseOptions.SectionName)
    .Get<DatabaseOptions>() ?? new DatabaseOptions();
var cacheOptions = builder.Configuration
    .GetSection(AppCacheOptions.SectionName)
    .Get<AppCacheOptions>() ?? new AppCacheOptions();
var officialDocumentOptions = builder.Configuration
    .GetSection(OfficialDocumentOptions.SectionName)
    .Get<OfficialDocumentOptions>() ?? new OfficialDocumentOptions();
var backgroundJobOptions = builder.Configuration
    .GetSection(BackgroundJobOptions.SectionName)
    .Get<BackgroundJobOptions>() ?? new BackgroundJobOptions();
var operationsOptions = builder.Configuration
    .GetSection(OperationsOptions.SectionName)
    .Get<OperationsOptions>() ?? new OperationsOptions();
var observabilityOptions = builder.Configuration
    .GetSection(ObservabilityOptions.SectionName)
    .Get<ObservabilityOptions>() ?? new ObservabilityOptions();
var performanceReportingOptions = builder.Configuration
    .GetSection(PerformanceReportingOptions.SectionName)
    .Get<PerformanceReportingOptions>() ?? new PerformanceReportingOptions();
var clickHouseAnalyticsOptions = builder.Configuration
    .GetSection(ClickHouseAnalyticsOptions.SectionName)
    .Get<ClickHouseAnalyticsOptions>() ?? new ClickHouseAnalyticsOptions();
var rabbitMqOptions = builder.Configuration
    .GetSection(RabbitMqOptions.SectionName)
    .Get<RabbitMqOptions>() ?? new RabbitMqOptions();
var ssoOptions = builder.Configuration
    .GetSection(SsoOptions.SectionName)
    .Get<SsoOptions>() ?? new SsoOptions();
var trustedProxyAddresses = builder.Configuration
    .GetSection("ReverseProxy:TrustedProxies")
    .Get<string[]>() ?? [];
var trustedProxies = trustedProxyAddresses
    .Select(value =>
    {
        if (!IPAddress.TryParse(value, out var address))
        {
            throw new InvalidOperationException(
                $"ReverseProxy:TrustedProxies contains an invalid IP address: '{value}'.");
        }

        return address;
    })
    .ToArray();

if (ssoOptions.Enabled &&
    (string.IsNullOrWhiteSpace(ssoOptions.ClientId) ||
     !Uri.TryCreate(ssoOptions.Authority, UriKind.Absolute, out var ssoAuthority) ||
     ssoAuthority.Scheme is not ("http" or "https") ||
     (ssoOptions.RequireHttpsMetadata && ssoAuthority.Scheme != "https") ||
     string.IsNullOrWhiteSpace(ssoOptions.UserNameClaim) ||
     (!string.IsNullOrWhiteSpace(ssoOptions.CallbackUrl) &&
      (!Uri.TryCreate(ssoOptions.CallbackUrl, UriKind.Absolute, out var callbackUrl) ||
       callbackUrl.Scheme is not ("http" or "https") ||
       !callbackUrl.AbsolutePath.EndsWith(
           "/signin-keycloak",
           StringComparison.OrdinalIgnoreCase))) ||
     (!string.IsNullOrWhiteSpace(ssoOptions.FrontendBaseUrl) &&
      (!Uri.TryCreate(ssoOptions.FrontendBaseUrl, UriKind.Absolute, out var frontendBaseUrl) ||
       frontendBaseUrl.Scheme is not ("http" or "https")))))
{
    throw new InvalidOperationException(
        "启用 Sso 时必须配置有效的 Authority、ClientId、UserNameClaim、CallbackUrl 和 FrontendBaseUrl；CallbackUrl 必须以 /signin-keycloak 结尾，生产元数据地址必须使用 HTTPS。");
}

if (string.IsNullOrWhiteSpace(officialDocumentOptions.InstitutionName) ||
    string.IsNullOrWhiteSpace(officialDocumentOptions.IssuingOffice) ||
    string.IsNullOrWhiteSpace(officialDocumentOptions.DocumentNumberPrefix))
{
    throw new InvalidOperationException(
        "OfficialDocuments 的 InstitutionName、IssuingOffice 和 DocumentNumberPrefix 不能为空。");
}

if (databaseOptions.Provider.Equals("SQLite", StringComparison.OrdinalIgnoreCase) &&
    !builder.Environment.IsDevelopment())
{
    throw new InvalidOperationException("SQLite 仅允许在 Development 环境使用。生产环境请配置 MySql。");
}

var allowedHosts = builder.Configuration["AllowedHosts"];
if (!builder.Environment.IsDevelopment() &&
    (string.IsNullOrWhiteSpace(allowedHosts) || allowedHosts == "*"))
{
    throw new InvalidOperationException(
        "生产环境必须通过 AllowedHosts 配置明确的访问域名，不能使用通配符。");
}

if (databaseOptions.CommandTimeoutSeconds is < 5 or > 300)
{
    throw new InvalidOperationException(
        "Database:CommandTimeoutSeconds 必须在 5 到 300 秒之间。");
}

if (string.IsNullOrWhiteSpace(observabilityOptions.ServiceName) ||
    observabilityOptions.ServiceName.Length > 100 ||
    observabilityOptions.SlowRequestThresholdMilliseconds is < 1 or > 60000 ||
    observabilityOptions.SlowQueryThresholdMilliseconds is < 1 or > 60000 ||
    observabilityOptions.MaximumSqlTextLength is < 256 or > 20000)
{
    throw new InvalidOperationException(
        "Observability 服务名、慢查询阈值或 SQL 文本长度超出允许范围。");
}

if (performanceReportingOptions.CacheSeconds is < 5 or > 300 ||
    performanceReportingOptions.TimeoutSeconds is < 1 or > 60 ||
    performanceReportingOptions.BearerToken.Length > 8000 ||
    !PerformanceReportingOptions.IsMetricOrLabelName(
        performanceReportingOptions.ServiceLabel) ||
    !PerformanceReportingOptions.IsMetricOrLabelName(
        performanceReportingOptions.RequestDurationMetric) ||
    !PerformanceReportingOptions.IsMetricOrLabelName(
        performanceReportingOptions.DatabaseDurationMetric) ||
    !PerformanceReportingOptions.IsMetricOrLabelName(
        performanceReportingOptions.SlowDatabaseMetric) ||
    !PerformanceReportingOptions.IsMetricOrLabelName(
        performanceReportingOptions.FailedDatabaseMetric) ||
    (performanceReportingOptions.Enabled &&
     !IsHttpUrl(performanceReportingOptions.PrometheusBaseUrl)) ||
    (!string.IsNullOrWhiteSpace(performanceReportingOptions.GrafanaBaseUrl) &&
     !IsHttpUrl(performanceReportingOptions.GrafanaBaseUrl)))
{
    throw new InvalidOperationException(
        "PerformanceReporting 数据源、超时、缓存或指标名称配置无效。");
}

if (cacheOptions.ReferenceExpirationMinutes is < 1 or > 1440 ||
    cacheOptions.TimetableExpirationMinutes is < 1 or > 1440 ||
    cacheOptions.AnalyticsExpirationMinutes is < 1 or > 1440 ||
    cacheOptions.ReferenceLocalExpirationSeconds is < 1 or > 3600 ||
    cacheOptions.TimetableLocalExpirationSeconds is < 1 or > 3600 ||
    cacheOptions.AnalyticsLocalExpirationSeconds is < 1 or > 3600 ||
    cacheOptions.MaximumPayloadKilobytes is < 64 or > 16384 ||
    cacheOptions.ReferenceLocalExpirationSeconds >
        cacheOptions.ReferenceExpirationMinutes * 60 ||
    cacheOptions.TimetableLocalExpirationSeconds >
        cacheOptions.TimetableExpirationMinutes * 60 ||
    cacheOptions.AnalyticsLocalExpirationSeconds >
        cacheOptions.AnalyticsExpirationMinutes * 60 ||
    string.IsNullOrWhiteSpace(cacheOptions.KeyPrefix) ||
    cacheOptions.KeyPrefix.Length > 100)
{
    throw new InvalidOperationException(
        "Cache 缓存时间或 MaximumPayloadKilobytes 超出允许范围。");
}

if (!backgroundJobOptions.Transport.Equals(
        "InMemory",
        StringComparison.OrdinalIgnoreCase) &&
    !backgroundJobOptions.UsesRabbitMq)
{
    throw new InvalidOperationException(
        "BackgroundJobs:Transport 仅支持 InMemory 或 RabbitMq。");
}
if (backgroundJobOptions.PollIntervalMilliseconds is < 100 or > 30000 ||
    backgroundJobOptions.LeaseSeconds is < 30 or > 3600 ||
    backgroundJobOptions.PrefetchCount is < 1 or > 100 ||
    backgroundJobOptions.AutomaticScheduleConcurrency is < 1 or > 16 ||
    backgroundJobOptions.SchedulePublishConcurrency is < 1 or > 16 ||
    backgroundJobOptions.MakeupExamAutoConcurrency is < 1 or > 16 ||
    backgroundJobOptions.ExamArrangementConcurrency is < 1 or > 16 ||
    backgroundJobOptions.ExamSignInExportConcurrency is < 1 or > 16 ||
    backgroundJobOptions.ExamPublishConcurrency is < 1 or > 16 ||
    backgroundJobOptions.CourseGradeStatisticsRefreshConcurrency is < 1 or > 16 ||
    backgroundJobOptions.ProcessingAttemptLimit is < 1 or > 100 ||
    backgroundJobOptions.MaintenanceIntervalSeconds is < 10 or > 3600 ||
    backgroundJobOptions.CompletedRetentionDays is < 1 or > 3650 ||
    backgroundJobOptions.CleanupBatchSize is < 10 or > 5000 ||
    string.IsNullOrWhiteSpace(backgroundJobOptions.Exchange) ||
    string.IsNullOrWhiteSpace(backgroundJobOptions.QueuePrefix))
{
    throw new InvalidOperationException("BackgroundJobs 配置超出允许范围。");
}
if (backgroundJobOptions.UsesRabbitMq &&
    (string.IsNullOrWhiteSpace(rabbitMqOptions.HostName) ||
     rabbitMqOptions.Port is < 1 or > 65535 ||
     string.IsNullOrWhiteSpace(rabbitMqOptions.UserName) ||
     string.IsNullOrWhiteSpace(rabbitMqOptions.Password) ||
     string.IsNullOrWhiteSpace(rabbitMqOptions.VirtualHost)))
{
    throw new InvalidOperationException("RabbitMq 连接配置不完整。");
}

if (clickHouseAnalyticsOptions.Enabled &&
    (!Uri.TryCreate(clickHouseAnalyticsOptions.Endpoint, UriKind.Absolute, out var clickHouseEndpoint) ||
     clickHouseEndpoint.Scheme is not ("http" or "https") ||
     !clickHouseAnalyticsOptions.HasValidIdentifiers() ||
     string.IsNullOrWhiteSpace(clickHouseAnalyticsOptions.UserName) ||
     string.IsNullOrWhiteSpace(clickHouseAnalyticsOptions.Password) ||
     clickHouseAnalyticsOptions.SyncIntervalSeconds is < 10 or > 86400 ||
     clickHouseAnalyticsOptions.SourceLookbackDays is < 1 or > 3650 ||
     clickHouseAnalyticsOptions.BatchSize is < 1 or > 10000))
{
    throw new InvalidOperationException("ClickHouseAnalytics 配置无效。");
}
if (backgroundJobOptions.UsesRabbitMq &&
    !builder.Environment.IsDevelopment() &&
    (rabbitMqOptions.UserName.Equals("guest", StringComparison.OrdinalIgnoreCase) ||
     rabbitMqOptions.Password == "guest"))
{
    throw new InvalidOperationException(
        "生产环境启用 RabbitMQ 时不能使用默认 guest 凭据。");
}

if (string.IsNullOrWhiteSpace(operationsOptions.BackupDirectory) ||
    operationsOptions.BackupWarningHours is < 1 or > 8760 ||
    operationsOptions.ToolTimeoutMinutes is < 1 or > 240 ||
    string.IsNullOrWhiteSpace(operationsOptions.MySqlDumpPath) ||
    string.IsNullOrWhiteSpace(operationsOptions.MySqlClientPath) ||
    operationsOptions.MySqlAdditionalArguments.Length > 20 ||
    operationsOptions.MySqlAdditionalArguments.Any(argument =>
        string.IsNullOrWhiteSpace(argument) ||
        argument.Length > 300 ||
        !argument.StartsWith("--", StringComparison.Ordinal)))
{
    throw new InvalidOperationException("Operations 运维与备份配置超出允许范围。");
}

builder.Services.AddSingleton(databaseOptions);
builder.Services.AddSingleton(cacheOptions);
builder.Services.AddSingleton(officialDocumentOptions);
builder.Services.AddSingleton(backgroundJobOptions);
builder.Services.AddSingleton(operationsOptions);
builder.Services.AddSingleton(observabilityOptions);
builder.Services.AddSingleton(performanceReportingOptions);
builder.Services.AddSingleton(clickHouseAnalyticsOptions);
builder.Services.AddSingleton(rabbitMqOptions);
builder.Services.AddSingleton<DatabaseCommandTelemetryInterceptor>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<PerformanceReportService>((services, client) =>
{
    var reporting = services.GetRequiredService<PerformanceReportingOptions>();
    client.Timeout = TimeSpan.FromSeconds(reporting.TimeoutSeconds);
});
builder.Services.AddHttpClient<ClickHouseAnalyticsClient>((_, client) =>
{
    client.BaseAddress = new Uri(clickHouseAnalyticsOptions.Endpoint.TrimEnd('/') + "/");
    client.Timeout = TimeSpan.FromSeconds(30);
});
builder.Services.Configure<OfficialDocumentOptions>(
    builder.Configuration.GetSection(OfficialDocumentOptions.SectionName));
builder.Services.AddDbContextPool<AppDbContext>((services, options) =>
{
    options.AddInterceptors(
        services.GetRequiredService<DatabaseCommandTelemetryInterceptor>());

    if (databaseOptions.Provider.Equals("SQLite", StringComparison.OrdinalIgnoreCase))
    {
        var sqliteConnectionString = builder.Configuration.GetConnectionString("SQLite")
            ?? throw new InvalidOperationException("缺少 ConnectionStrings:SQLite。");
        var sqliteBuilder = new SqliteConnectionStringBuilder(sqliteConnectionString);
        if (!Path.IsPathRooted(sqliteBuilder.DataSource))
        {
            sqliteBuilder.DataSource = Path.GetFullPath(
                sqliteBuilder.DataSource,
                builder.Environment.ContentRootPath);
        }
        Directory.CreateDirectory(
            Path.GetDirectoryName(sqliteBuilder.DataSource)
            ?? builder.Environment.ContentRootPath);
        options.UseSqlite(sqliteBuilder.ConnectionString);
        return;
    }

    if (!databaseOptions.Provider.Equals("MySql", StringComparison.OrdinalIgnoreCase))
    {
        throw new InvalidOperationException(
            $"不支持数据库 Provider '{databaseOptions.Provider}'，可选值为 SQLite、MySql。");
    }

    var connectionString = builder.Configuration.GetConnectionString("MySql");
    if (string.IsNullOrWhiteSpace(connectionString))
    {
        throw new InvalidOperationException(
            "缺少 MySQL 连接串。请通过 ConnectionStrings__MySql 环境变量配置。");
    }
    options.UseMySQL(
        MySqlConnectionStringPolicy.ForApplication(connectionString),
        mySqlOptions =>
        {
            mySqlOptions.CommandTimeout(databaseOptions.CommandTimeoutSeconds);
            mySqlOptions.EnableRetryOnFailure(
                maxRetryCount: 5,
                maxRetryDelay: TimeSpan.FromSeconds(10),
                errorNumbersToAdd: null);
        });
});

var redisConnectionString = builder.Configuration.GetConnectionString("Redis");
if (cacheOptions.Enabled && !string.IsNullOrWhiteSpace(redisConnectionString))
{
    builder.Services.AddStackExchangeRedisCache(options =>
        options.Configuration = redisConnectionString);
}
else
{
    builder.Services.AddDistributedMemoryCache();
}
builder.Services.AddHybridCache(options =>
{
    options.MaximumKeyLength = 512;
    options.MaximumPayloadBytes = cacheOptions.MaximumPayloadKilobytes * 1024;
});
builder.Services.AddSingleton<IAppCache, HybridAppCache>();

builder.Services
    .AddIdentityCore<ApplicationUser>(options =>
    {
        options.Password.RequiredLength = 8;
        options.Password.RequireDigit = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireUppercase = true;
        options.Password.RequireNonAlphanumeric = true;
        options.User.RequireUniqueEmail = false;
        options.Lockout.MaxFailedAccessAttempts = 5;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
    })
    .AddRoles<ApplicationRole>()
    .AddEntityFrameworkStores<AppDbContext>();

var jwtOptions = builder.Configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>()
    ?? throw new InvalidOperationException("缺少 Jwt 配置。");
if (Encoding.UTF8.GetByteCount(jwtOptions.Key) < 32 ||
    jwtOptions.Key.Contains("REPLACE", StringComparison.OrdinalIgnoreCase))
{
    throw new InvalidOperationException(
        "Jwt:Key 必须配置为至少 32 字节的随机生产密钥，不能使用示例值。");
}
if (jwtOptions.AccessTokenMinutes is < 1 or > 30 ||
    jwtOptions.WebIdleMinutes is < 5 or > 1440 ||
    jwtOptions.AppIdleMinutes is < 60 or > 43200 ||
    jwtOptions.AccessTokenMinutes > jwtOptions.WebIdleMinutes)
{
    throw new InvalidOperationException(
        "Jwt 访问令牌或 Web/App 空闲有效期配置超出允许范围。");
}

builder.Services.Configure<JwtOptions>(
    builder.Configuration.GetSection(JwtOptions.SectionName));
builder.Services.Configure<SsoOptions>(
    builder.Configuration.GetSection(SsoOptions.SectionName));
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITokenService, TokenService>();
builder.Services.AddScoped<IAuthSessionService, AuthSessionService>();
builder.Services.AddScoped<ICurrentUserDataScope, CurrentUserDataScope>();
builder.Services.AddScoped<DatabaseInitializer>();
builder.Services.AddScoped<DemoDataSeeder>();
builder.Services.AddScoped<DevelopmentSqliteMigrator>();
builder.Services.AddScoped<TimetableDataService>();
builder.Services.AddScoped<PublishedTimetableProjectionService>();
builder.Services.AddScoped<AutomaticScheduleGenerator>();
builder.Services.AddScoped<PersonalCalendarService>();
builder.Services.AddScoped<ClassroomReservationAvailabilityService>();
builder.Services.AddScoped<AutomaticScheduleJobProcessor>();
builder.Services.AddScoped<SchedulePlanPublisher>();
builder.Services.AddScoped<SchedulePublishJobProcessor>();
builder.Services.AddHostedService<WarningCheckWorker>();
builder.Services.AddScoped<ExamArrangementService>();
builder.Services.AddScoped<MakeupExamEligibilityService>();
builder.Services.AddScoped<MakeupExamArrangementService>();
builder.Services.AddScoped<MakeupExamAutoJobProcessor>();
builder.Services.AddScoped<ExamArrangementJobProcessor>();
builder.Services.AddScoped<ExamSignInExportJobProcessor>();
builder.Services.AddScoped<ExamPublishJobProcessor>();
builder.Services.AddScoped<CourseGradeStatisticsRefreshJobProcessor>();
builder.Services.AddScoped<CourseGradeStatisticsRefreshScheduler>();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddHostedService<CourseGradeStatisticsRefreshWorker>();
builder.Services.AddHostedService<ClickHouseAnalyticsProjectionWorker>();
builder.Services.AddSingleton<BackgroundJobTelemetry>();
builder.Services.AddScoped<BackgroundJobMonitoringService>();
builder.Services.AddScoped<OperationalHealthService>();
builder.Services.AddSingleton<DatabaseBackupService>();
builder.Services.AddSingleton<BackgroundJobRunner>();
if (backgroundJobOptions.UsesRabbitMq)
{
    builder.Services.AddSingleton<
        IBackgroundJobTransport,
        RabbitMqBackgroundJobTransport>();
    builder.Services.AddHostedService<RabbitMqBackgroundJobWorker>();
}
else
{
    builder.Services.AddSingleton<InMemoryBackgroundJobTransport>();
    builder.Services.AddSingleton<IBackgroundJobTransport>(services =>
        services.GetRequiredService<InMemoryBackgroundJobTransport>());
    builder.Services.AddHostedService<InMemoryBackgroundJobWorker>();
}
builder.Services.AddHostedService<BackgroundJobOutboxPublisher>();
builder.Services.AddSingleton<IOfficialDocumentPdfGenerator, OfficialDocumentPdfGenerator>();
builder.Services.AddScoped<OfficialDocumentService>();

var authentication = builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = jwtOptions.Issuer,
            ValidAudience = jwtOptions.Audience,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(jwtOptions.Key)),
            ClockSkew = TimeSpan.FromMinutes(1)
        };
    })
    .AddCookie(SsoAuthSchemes.ExternalCookie, options =>
    {
        options.Cookie.Name = "__Host-jiaowu-sso";
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
    });
if (ssoOptions.Enabled)
{
    authentication.AddOpenIdConnect(SsoAuthSchemes.Keycloak, options =>
    {
        options.Authority = ssoOptions.Authority.TrimEnd('/');
        options.ClientId = ssoOptions.ClientId;
        options.ClientSecret = ssoOptions.ClientSecret;
        options.SignInScheme = SsoAuthSchemes.ExternalCookie;
        options.ResponseType = "code";
        options.UsePkce = true;
        options.SaveTokens = false;
        options.RequireHttpsMetadata = ssoOptions.RequireHttpsMetadata;
        options.CallbackPath = "/signin-keycloak";
        options.GetClaimsFromUserInfoEndpoint = true;
        options.MapInboundClaims = false;
        options.ClaimActions.MapUniqueJsonKey(
            ssoOptions.UserNameClaim,
            ssoOptions.UserNameClaim);
        options.TokenValidationParameters.NameClaimType = ssoOptions.UserNameClaim;
        options.Events.OnRedirectToIdentityProvider = context =>
        {
            if (!string.IsNullOrWhiteSpace(ssoOptions.CallbackUrl))
                context.ProtocolMessage.RedirectUri = ssoOptions.CallbackUrl;
            return Task.CompletedTask;
        };
        options.Events.OnRemoteFailure = context =>
        {
            context.HandleResponse();
            var loginUrl = string.IsNullOrWhiteSpace(ssoOptions.FrontendBaseUrl)
                ? "/login"
                : ssoOptions.FrontendBaseUrl.TrimEnd('/') + "/login";
            context.Response.Redirect(loginUrl + "?ssoError=authentication_failed");
            return Task.CompletedTask;
        };
    });
}
builder.Services.AddAuthorization();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    options.ForwardLimit = 1;
    options.KnownIPNetworks.Clear();
    options.KnownProxies.Clear();
    foreach (var proxy in trustedProxies)
    {
        options.KnownProxies.Add(proxy);
    }
});
builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.AddPolicy("public-auth", context =>
        RateLimitPartition.GetFixedWindowLimiter(
            context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
            _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 10,
                Window = TimeSpan.FromMinutes(5),
                QueueLimit = 0,
                AutoReplenishment = true
            }));
    options.AddPolicy("token-refresh", context =>
        RateLimitPartition.GetFixedWindowLimiter(
            context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
            _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 600,
                Window = TimeSpan.FromMinutes(1),
                QueueLimit = 0,
                AutoReplenishment = true
            }));
    options.AddPolicy("official-verification", context =>
        RateLimitPartition.GetFixedWindowLimiter(
            context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
            _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 30,
                Window = TimeSpan.FromMinutes(1),
                QueueLimit = 0,
                AutoReplenishment = true
            }));
    options.AddPolicy("app-updates", context =>
        RateLimitPartition.GetFixedWindowLimiter(
            context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
            _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 120,
                Window = TimeSpan.FromMinutes(1),
                QueueLimit = 0,
                AutoReplenishment = true
            }));
});

builder.Services.AddCors(options =>
{
    options.AddPolicy("Web", policy =>
    {
        var origins = builder.Configuration.GetSection("Cors:Origins").Get<string[]>()
            ?? [];
        if (origins.Length > 0)
        {
            policy.WithOrigins(origins)
                .AllowAnyHeader()
                .AllowAnyMethod();
        }
    });
});

builder.Services.AddResponseCompression(options => options.EnableForHttps = true);
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler(options =>
{
    options.ExceptionHandler = async context =>
    {
        var exception = context.Features
            .Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>()?.Error;
        var isConstraintConflict = exception is DbUpdateException;
        var statusCode = isConstraintConflict
            ? StatusCodes.Status409Conflict
            : StatusCodes.Status500InternalServerError;
        context.Response.StatusCode = statusCode;
        await Results.Problem(
                title: isConstraintConflict ? "数据约束冲突" : "服务器处理请求时发生错误",
                detail: isConstraintConflict
                    ? "编码可能已存在，或该数据正在被其他业务引用。"
                    : builder.Environment.IsDevelopment()
                        ? exception?.Message
                        : "请稍后重试，并联系系统管理员查看日志。",
                statusCode: statusCode)
            .ExecuteAsync(context);
    };
});

builder.Services.AddControllers()
    .AddJsonOptions(options =>
        options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc(swaggerDocumentVersion, new OpenApiInfo
    {
        Title = "大学教务管理系统 API",
        Version = swaggerDocumentVersion
    });
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = "bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header
    });
    options.AddSecurityRequirement(_ => new OpenApiSecurityRequirement
    {
        [
            new OpenApiSecuritySchemeReference("Bearer", null, null)
        ] = []
    });
});

var app = builder.Build();

app.UseForwardedHeaders();
app.UseExceptionHandler();
app.UseResponseCompression();
app.Use(async (context, next) =>
{
    if (context.Request.Path.StartsWithSegments("/swagger"))
    {
        var isEnabled = await context.RequestServices
            .GetRequiredService<AppDbContext>()
            .SystemFeatureSettings
            .AsNoTracking()
            .Where(x => x.Key == SystemFeatureKeys.SwaggerDocumentation)
            .Select(x => (bool?)x.IsEnabled)
            .SingleOrDefaultAsync(context.RequestAborted) ?? false;
        if (!isEnabled)
        {
            context.Response.StatusCode = StatusCodes.Status404NotFound;
            return;
        }
    }

    await next();
});
app.UseSwagger();
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint(
        $"/swagger/{swaggerDocumentVersion}/swagger.json",
        $"大学教务管理系统 API {swaggerDocumentVersion}");
    options.DocExpansion(DocExpansion.None);
    options.DefaultModelsExpandDepth(-1);
    options.DefaultModelExpandDepth(1);
    options.EnableFilter();
});

app.UseDefaultFiles();
app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = context =>
    {
        if (context.Context.Request.Path.StartsWithSegments("/assets"))
        {
            context.Context.Response.Headers.CacheControl =
                "public,max-age=31536000,immutable";
        }
        else if (string.Equals(
                     Path.GetExtension(context.File.Name),
                     ".html",
                     StringComparison.OrdinalIgnoreCase))
        {
            context.Context.Response.Headers.CacheControl = "no-cache,no-store";
        }
    }
});
app.UseCors("Web");
app.UseRateLimiter();
app.UseMiddleware<SlowRequestLoggingMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<AuditMiddleware>();
app.MapControllers();
app.MapGet("/health/live", () => Results.Ok(new { Status = "healthy" }))
    .AllowAnonymous();
app.MapGet("/health", CheckDatabaseHealthAsync).AllowAnonymous();
app.MapGet("/health/ready", CheckDatabaseHealthAsync).AllowAnonymous();
app.MapGet("/health/cache", CheckCacheHealthAsync).AllowAnonymous();
app.MapGet("/health/messaging", CheckMessagingHealthAsync).AllowAnonymous();
app.MapFallback(async context =>
{
    if (context.Request.Path.StartsWithSegments("/api") ||
        context.Request.Path.StartsWithSegments("/health") ||
        context.Request.Path.StartsWithSegments("/swagger"))
    {
        context.Response.StatusCode = StatusCodes.Status404NotFound;
        return;
    }

    var webRoot = app.Environment.WebRootPath
        ?? Path.Combine(app.Environment.ContentRootPath, "wwwroot");
    var indexPath = Path.Combine(webRoot, "index.html");
    if (!File.Exists(indexPath))
    {
        context.Response.StatusCode = StatusCodes.Status404NotFound;
        return;
    }

    context.Response.Headers.CacheControl = "no-cache,no-store";
    context.Response.ContentType = "text/html; charset=utf-8";
    await context.Response.SendFileAsync(indexPath);
});

using (var scope = app.Services.CreateScope())
{
    await scope.ServiceProvider.GetRequiredService<DatabaseInitializer>()
        .InitializeAsync(migrateOnly);

    if (seedDemoData)
    {
        await scope.ServiceProvider.GetRequiredService<DemoDataSeeder>()
            .SeedAsync();
    }
}

if (migrateOnly || seedDemoData)
{
    return;
}

app.Run();

static async Task<IResult> CheckDatabaseHealthAsync(
    AppDbContext db,
    CancellationToken cancellationToken)
{
    try
    {
        return await db.Database.CanConnectAsync(cancellationToken)
            ? Results.Ok(new { Status = "healthy" })
            : Results.Json(
                new { Status = "unhealthy" },
                statusCode: StatusCodes.Status503ServiceUnavailable);
    }
    catch
    {
        return Results.Json(
            new { Status = "unhealthy" },
            statusCode: StatusCodes.Status503ServiceUnavailable);
    }
}

static async Task<IResult> CheckCacheHealthAsync(
    IServiceProvider services,
    CancellationToken cancellationToken)
{
    var distributedCache = services.GetService<IDistributedCache>();
    if (distributedCache is null)
        return Results.Ok(new { Status = "disabled", Backend = "memory" });

    try
    {
        await distributedCache.GetAsync(
            "jiaowu:health:probe",
            cancellationToken);
        return Results.Ok(new { Status = "healthy", Backend = "redis" });
    }
    catch
    {
        return Results.Json(
            new { Status = "unhealthy", Backend = "redis" },
            statusCode: StatusCodes.Status503ServiceUnavailable);
    }
}

static async Task<IResult> CheckMessagingHealthAsync(
    IBackgroundJobTransport transport,
    BackgroundJobMonitoringService monitoring,
    CancellationToken cancellationToken)
{
    var healthy = await transport.CheckHealthAsync(cancellationToken);
    var backend = transport.IsDurable ? "rabbitmq" : "memory";
    try
    {
        var backlog = await monitoring.GetSnapshotAsync(cancellationToken);
        return healthy
            ? Results.Ok(new
            {
                Status = "healthy",
                Backend = backend,
                Backlog = backlog
            })
            : Results.Json(
                new
                {
                    Status = "unhealthy",
                    Backend = backend,
                    Backlog = backlog
                },
                statusCode: StatusCodes.Status503ServiceUnavailable);
    }
    catch
    {
        return Results.Json(
            new
            {
                Status = "unhealthy",
                Backend = backend,
                Backlog = (object?)null
            },
            statusCode: StatusCodes.Status503ServiceUnavailable);
    }
}

static bool IsHttpUrl(string value) =>
    Uri.TryCreate(value, UriKind.Absolute, out var uri) &&
    uri.Scheme is "http" or "https";

public partial class Program;
