using System.Security.Claims;
using foodmarket.Api.Infrastructure.Tenancy;
using foodmarket.Api.Seed;
using foodmarket.Application.Common.Tenancy;
using foodmarket.Infrastructure.Identity;
using foodmarket.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using OpenIddict.Validation.AspNetCore;
using Serilog;
using static OpenIddict.Abstractions.OpenIddictConstants;

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateBootstrapLogger();

try
{
    var builder = WebApplication.CreateBuilder(args);

    builder.Host.UseSerilog((ctx, services, cfg) =>
        cfg.ReadFrom.Configuration(ctx.Configuration).ReadFrom.Services(services));

    const string CorsPolicy = "food-market-cors";
    builder.Services.AddCors(opts => opts.AddPolicy(CorsPolicy, p =>
    {
        var origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>()
                      ?? ["http://localhost:5173"];
        p.WithOrigins(origins).AllowAnyHeader().AllowAnyMethod().AllowCredentials();
    }));

    builder.Services.AddHttpContextAccessor();
    builder.Services.AddScoped<ITenantContext, HttpContextTenantContext>();

    builder.Services.AddDbContext<AppDbContext>(opts =>
    {
        opts.UseNpgsql(builder.Configuration.GetConnectionString("Default"),
            npg => npg.MigrationsAssembly(typeof(AppDbContext).Assembly.GetName().Name));
        opts.UseOpenIddict();
    });

    builder.Services.AddIdentity<User, Role>(opts =>
        {
            opts.Password.RequireDigit = true;
            opts.Password.RequiredLength = 8;
            opts.Password.RequireNonAlphanumeric = false;
            opts.Password.RequireUppercase = false;
            opts.User.RequireUniqueEmail = true;
        })
        .AddEntityFrameworkStores<AppDbContext>()
        .AddDefaultTokenProviders();

    builder.Services.Configure<IdentityOptions>(opts =>
    {
        opts.ClaimsIdentity.UserNameClaimType = Claims.Name;
        opts.ClaimsIdentity.UserIdClaimType = Claims.Subject;
        opts.ClaimsIdentity.RoleClaimType = Claims.Role;
    });

    builder.Services.AddOpenIddict()
        .AddCore(opts => opts.UseEntityFrameworkCore().UseDbContext<AppDbContext>())
        .AddServer(opts =>
        {
            opts.SetTokenEndpointUris("/connect/token");
            opts.AllowPasswordFlow().AllowRefreshTokenFlow();
            opts.AcceptAnonymousClients();
            opts.RegisterScopes(Scopes.OpenId, Scopes.Profile, Scopes.Email, Scopes.Roles, "api");

            // Persistent dev keys: RSA key stored in src/food-market.api/App_Data/openiddict-dev-key.xml.
            // Survives API restarts so issued tokens remain valid across rebuilds.
            // Never commit this file (it's in .gitignore via App_Data/). Production must use real certificates.
            var keyPath = Path.Combine(builder.Environment.ContentRootPath, "App_Data", "openiddict-dev-key.xml");
            var rsa = System.Security.Cryptography.RSA.Create(2048);
            if (File.Exists(keyPath))
            {
                rsa.FromXmlString(File.ReadAllText(keyPath));
            }
            else
            {
                Directory.CreateDirectory(Path.GetDirectoryName(keyPath)!);
                File.WriteAllText(keyPath, rsa.ToXmlString(includePrivateParameters: true));
            }
            var devKey = new Microsoft.IdentityModel.Tokens.RsaSecurityKey(rsa) { KeyId = "food-market-dev" };
            opts.AddEncryptionKey(devKey);
            opts.AddSigningKey(devKey);
            if (builder.Environment.IsDevelopment())
            {
                opts.DisableAccessTokenEncryption();
            }

            opts.UseAspNetCore()
                .EnableTokenEndpointPassthrough()
                .DisableTransportSecurityRequirement();

            opts.SetAccessTokenLifetime(TimeSpan.Parse(
                builder.Configuration["OpenIddict:AccessTokenLifetime"] ?? "01:00:00"));
            opts.SetRefreshTokenLifetime(TimeSpan.Parse(
                builder.Configuration["OpenIddict:RefreshTokenLifetime"] ?? "30.00:00:00"));
            // Явный Issuer = публичный URL админки. Без него OpenIddict
            // вычисляет issuer из текущего HTTP-запроса, что ломается за
            // nginx-прокси если Host/X-Forwarded-Proto не доходят. Берём
            // из конфигурации (OpenIddict:Issuer / docker compose env).
            if (builder.Configuration["OpenIddict:Issuer"] is { Length: > 0 } issuer)
            {
                opts.SetIssuer(new Uri(issuer));
            }
        })
        .AddValidation(opts =>
        {
            opts.UseLocalServer();
            opts.UseAspNetCore();
        });

    // Force OpenIddict validation to handle ALL [Authorize] challenges — otherwise AddIdentity's
    // cookie scheme takes over and 401 becomes a 302 redirect to /Account/Login for API calls.
    builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
        options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
    });
    builder.Services.AddAuthorization(opts =>
    {
        // Check the "role" claim explicitly — robust against role-claim-type mismatches between
        // OpenIddict validation identity and the default ClaimTypes.Role uri.
        opts.AddPolicy("AdminAccess", p => p.RequireAssertion(ctx =>
            ctx.User.HasClaim(c => c.Type == Claims.Role && (c.Value == "Admin" || c.Value == "SuperAdmin"))));
    });

    builder.Services.AddScoped<foodmarket.Api.Infrastructure.Tenancy.SuperAdminEditAuditFilter>();

    // Email-отправка через MailKit. Singleton — внутри открывает scope для
    // свежего DbContext'а, поэтому конфиг (PlatformSettings) перечитывается
    // на каждой отправке без рестарта приложения.
    builder.Services.AddSingleton<foodmarket.Application.Common.Email.IEmailSender,
        foodmarket.Infrastructure.Email.MailKitEmailSender>();
    builder.Services.AddDataProtection();
    builder.Services.AddControllers(o =>
    {
        // Глобальный action filter — пишет audit-log при успешных мутациях
        // в режиме «SuperAdmin открыть как… + edit-mode» (Phase 3).
        o.Filters.AddService<foodmarket.Api.Infrastructure.Tenancy.SuperAdminEditAuditFilter>();
    });
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();

    // MoySklad import integration. Auto-decompress gzip responses from MoySklad's edge.
    builder.Services.AddHttpClient<foodmarket.Infrastructure.Integrations.MoySklad.MoySkladClient>()
        .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
        {
            AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate,
        });
    builder.Services.AddScoped<foodmarket.Infrastructure.Integrations.MoySklad.MoySkladImportService>();
    builder.Services.AddSingleton<foodmarket.Infrastructure.Integrations.MoySklad.ImportJobRegistry>();

    // Inventory
    builder.Services.AddScoped<foodmarket.Application.Inventory.IStockService, foodmarket.Infrastructure.Inventory.StockService>();

    builder.Services.AddHostedService<OpenIddictClientSeeder>();
    builder.Services.AddHostedService<SystemReferenceSeeder>();
    builder.Services.AddHostedService<DevDataSeeder>();
    builder.Services.AddHostedService<foodmarket.Api.Background.ReferencePriceRefreshJob>();
    // DemoCatalogSeeder disabled: real catalog is imported from MoySklad.
    // Keep the file as reference for anyone starting without MoySklad access —
    // just re-register here to turn demo data back on.
    // builder.Services.AddHostedService<DemoCatalogSeeder>();

    var app = builder.Build();

    app.UseSerilogRequestLogging();
    app.UseCors(CorsPolicy);
    app.UseAuthentication();
    app.UseAuthorization();
    // SuperAdmin «открыть как…» — тот же tenant как у выбранной орги, но
    // только GET. Любая мутация → 403, кроме /api/super-admin/* и /connect/*.
    app.UseMiddleware<foodmarket.Api.Infrastructure.Tenancy.ReadonlyOverrideMiddleware>();

    // Статика товарных изображений: физически /app/uploads (volume в compose),
    // публичный URL /uploads/... — раздаются public, без auth.
    var uploadsDir = System.IO.Path.Combine(app.Environment.ContentRootPath, "uploads");
    System.IO.Directory.CreateDirectory(uploadsDir);
    app.UseStaticFiles(new Microsoft.AspNetCore.Builder.StaticFileOptions
    {
        FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(uploadsDir),
        RequestPath = "/uploads",
    });

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

    app.MapControllers();

    app.MapGet("/health", () => Results.Ok(new { status = "ok", time = DateTime.UtcNow }));

    app.MapGet("/api/_debug/whoami", (HttpContext ctx) =>
    {
        var identity = ctx.User.Identity as System.Security.Claims.ClaimsIdentity;
        return Results.Ok(new
        {
            isAuthenticated = ctx.User.Identity?.IsAuthenticated,
            authType = ctx.User.Identity?.AuthenticationType,
            nameClaimType = identity?.NameClaimType,
            roleClaimType = identity?.RoleClaimType,
            isInRoleAdmin = ctx.User.IsInRole("Admin"),
            hasAdminRoleClaim = ctx.User.HasClaim(c => c.Type == Claims.Role && c.Value == "Admin"),
            claims = ctx.User.Claims.Select(c => new { c.Type, c.Value }),
        });
    }).RequireAuthorization();

    app.MapGet("/api/me", async (HttpContext ctx, AppDbContext db) =>
    {
        var user = ctx.User;
        var roles = user.FindAll(Claims.Role).Select(c => c.Value).ToList();
        var orgIdRaw = user.FindFirst(HttpContextTenantContext.OrganizationClaim)?.Value;
        var subRaw = user.FindFirst(Claims.Subject)?.Value;

        // Дополнительные сигналы, чтобы фронт точно знал в каком состоянии юзер
        // находится: orphan AppUser без живой org или без активного Employee
        // получает /no-organization fallback вместо белого экрана на /dashboard.
        bool hasLiveOrg = false;
        bool hasActiveEmployee = false;
        if (Guid.TryParse(orgIdRaw, out var orgId))
        {
            hasLiveOrg = await db.Organizations.IgnoreQueryFilters()
                .AnyAsync(o => o.Id == orgId && !o.IsArchived);
            if (Guid.TryParse(subRaw, out var sub))
            {
                hasActiveEmployee = await db.Employees.IgnoreQueryFilters()
                    .AnyAsync(e => e.OrganizationId == orgId && e.UserId == sub && e.IsActive);
            }
        }

        return Results.Ok(new
        {
            sub = subRaw,
            name = user.Identity?.Name,
            email = user.FindFirst(Claims.Email)?.Value,
            roles,
            orgId = orgIdRaw,
            hasLiveOrg,
            hasActiveEmployee,
        });
    }).RequireAuthorization();

    // Apply migrations on every startup (idempotent). Without this, fresh
    // stage/prod deploys land on an empty DB and OpenIddict seeders fail.
    using (var scope = app.Services.CreateScope())
    {
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        db.Database.Migrate();
    }

    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}
