using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using OmsorgCore.Api.Security;
using OmsorgCore.Application;
using OmsorgCore.Application.Abstractions;
using OmsorgCore.Email;
using OmsorgCore.Engine;
using OmsorgCore.Infrastructure;
using OmsorgCore.Infrastructure.Persistence;
using OmsorgCore.Infrastructure.Security;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header
    });
    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            Array.Empty<string>()
        }
    });
});

// Schichten verdrahten: Domain kennt niemanden, Application kennt nur Domain,
// Infrastructure implementiert Application-Interfaces, Engine ist die Event-Schicht
// auf denselben Daten, Api verdrahtet alles nur hier.
builder.Services.AddMemoryCache();
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddEngine();
builder.Services.AddEmail(builder.Configuration);

builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();

// Nötig, seit omsorgapp als Browser-SPA statt Electron läuft: der Refresh-Token geht per
// HttpOnly-Cookie, dafür muss der Browser die Cross-Origin-Antwort mit Credentials akzeptieren.
var corsOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
builder.Services.AddCors(options =>
{
    options.AddPolicy("Frontend", policy => policy
        .WithOrigins(corsOrigins)
        .AllowAnyHeader()
        .AllowAnyMethod()
        .AllowCredentials());
});

var jwtOptions = builder.Configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>() ?? new JwtOptions();

builder.Services
    .AddAuthentication(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.Secret))
        };

        // Killswitch: ein JWT ist an sich zustandslos gültig bis zum Ablauf. Damit ein erzwungener
        // Logout (SessionAdminService) SOFORT wirkt und nicht erst nach bis zu 60 Minuten, wird bei
        // jedem Request der "sst"-Claim gegen den aktuellen User.SecurityStamp in der DB geprüft.
        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = async context =>
            {
                var userIdClaim = context.Principal?.FindFirstValue(JwtRegisteredClaimNames.Sub)
                    ?? context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
                var stampClaim = context.Principal?.FindFirstValue("sst");

                if (!Guid.TryParse(userIdClaim, out var userId) || string.IsNullOrEmpty(stampClaim))
                {
                    context.Fail("Token ohne gültige Benutzer-/Security-Stamp-Claims.");
                    return;
                }

                var userRepository = context.HttpContext.RequestServices.GetRequiredService<IUserRepository>();
                var user = await userRepository.GetByIdAsync(userId);

                if (user is null || !user.IsActive || user.SecurityStamp.ToString() != stampClaim)
                {
                    context.Fail("Session wurde widerrufen.");
                    return;
                }

                // Erzwingt einen Passwortwechsel serverseitig (nicht nur im UI versteckt) - solange
                // MustChangePassword gesetzt ist (Admin hat ein initiales Passwort direkt vergeben),
                // sind nur noch change-password/logout/refresh/me erlaubt. Frischer DB-Wert statt
                // Claim, damit ein erfolgreicher Wechsel sofort wirkt und nicht erst nach Token-Ablauf.
                if (user.MustChangePassword)
                {
                    var path = context.HttpContext.Request.Path;
                    var isAllowlisted = path.StartsWithSegments("/api/auth/change-password")
                        || path.StartsWithSegments("/api/auth/logout")
                        || path.StartsWithSegments("/api/auth/refresh")
                        || path.StartsWithSegments("/api/auth/me");

                    if (!isAllowlisted)
                    {
                        context.Fail("Passwort muss vor weiteren Aktionen geändert werden.");
                    }
                }
            }
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

// Wendet beim Start ausstehende Migrationen an - in jeder Umgebung, nicht nur Development,
// damit ein Deploy/Update nie mit einem Schema laufen kann, das hinter dem Code zurückliegt.
using (var migrationScope = app.Services.CreateScope())
{
    var db = migrationScope.ServiceProvider.GetRequiredService<OmsorgCoreDbContext>();
    await db.Database.MigrateAsync();
}

// Basis-Rollen (Geschäftsführung/Disposition/Recruiting/Außendienst, siehe REQUIREMENTS.md Abschnitt 3/7)
// in jeder Umgebung anlegen - reine Rollen-Stammdaten ohne Zugangsdaten, anders als der
// Development-only-Admin-Seed unten. Nicht fatal bei Fehlern, analog zum Admin-Seed.
try
{
    using var baseRolesScope = app.Services.CreateScope();
    var db = baseRolesScope.ServiceProvider.GetRequiredService<OmsorgCoreDbContext>();
    await DbSeeder.SeedBaseRolesAsync(db);
}
catch (Exception ex)
{
    app.Logger.LogWarning(ex, "Seed der Basis-Rollen fehlgeschlagen (z.B. Datenbank nicht erreichbar).");
}

// Konfigurierbare Auswahllisten (Mitarbeiterstatus, Beschäftigungsart, CRM-Status, Einrichtungstyp,
// Vertragstyp/-status, Auftragsstatus inkl. Übergangsregeln, FR-EM-2) - reine Stammdaten, jede
// Umgebung, nicht fatal.
try
{
    using var valueListScope = app.Services.CreateScope();
    var db = valueListScope.ServiceProvider.GetRequiredService<OmsorgCoreDbContext>();
    await DbSeeder.SeedValueListsAsync(db);
}
catch (Exception ex)
{
    app.Logger.LogWarning(ex, "Seed der Auswahllisten fehlgeschlagen (z.B. Datenbank nicht erreichbar).");
}

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();

    try
    {
        using var seedScope = app.Services.CreateScope();
        var db = seedScope.ServiceProvider.GetRequiredService<OmsorgCoreDbContext>();
        var passwordHasher = seedScope.ServiceProvider.GetRequiredService<IPasswordHasher>();
        var seedOptions = seedScope.ServiceProvider.GetRequiredService<IOptions<SeedOptions>>().Value;
        await DbSeeder.SeedDefaultAdminAsync(db, passwordHasher, seedOptions);
    }
    catch (Exception ex)
    {
        app.Logger.LogWarning(ex, "Seed des Standard-Admin-Users fehlgeschlagen (z.B. Datenbank nicht erreichbar).");
    }
}

app.UseHttpsRedirection();

app.UseCors("Frontend");

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

app.MapControllers();

app.Run();
