using System.Threading.RateLimiting;
using Backend.Common;
using Backend.Configuration;
using Backend.Data;
using Backend.Repositories;
using Backend.Security;
using Backend.Services;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using System.Globalization;

namespace Backend.Extensions;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(
        this IServiceCollection services,
        IConfiguration configuration,
        IWebHostEnvironment environment)
    {
        services.AddProblemDetails();
        services.AddEndpointsApiExplorer();
        services.AddSwaggerGen();

        services.Configure<FrontendOptions>(configuration.GetSection(FrontendOptions.SectionName));
        services.Configure<TwitchAuthOptions>(configuration.GetSection(TwitchAuthOptions.SectionName));
        services.AddMemoryCache();
        services.AddHttpClient();
        services.AddHttpClient("TwitchTracker", client =>
        {
            client.Timeout = TimeSpan.FromSeconds(4);
            client.DefaultRequestHeaders.UserAgent.ParseAdd("VTuberStarAwards/1.0");
        });
        var allowedOrigins = ResolveAllowedOrigins(configuration, environment);

        var connectionString = configuration["VTSA_POSTGRES"] ?? configuration.GetConnectionString("Postgres");
        if (string.IsNullOrWhiteSpace(connectionString))
        {
            throw new InvalidOperationException(
                "No PostgreSQL connection string configured. Set VTSA_POSTGRES or ConnectionStrings:Postgres.");
        }

        services.AddCors(options =>
        {
            options.AddPolicy(ApplicationDefaults.FrontendCorsPolicy, policy =>
            {
                policy.WithOrigins(allowedOrigins)
                    .WithHeaders("Authorization", "Content-Type")
                    .WithMethods(HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete);
            });
        });

        services.AddRateLimiter(options =>
        {
            options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
            options.OnRejected = async (context, cancellationToken) =>
            {
                context.HttpContext.Response.ContentType = "application/json";
                await context.HttpContext.Response.WriteAsJsonAsync(
                    new { message = "Zu viele Anfragen. Bitte kurz warten und erneut versuchen." },
                    cancellationToken);
            };

            options.AddPolicy(ApplicationDefaults.AuthRateLimitPolicy, context =>
                RateLimitPartition.GetFixedWindowLimiter(
                    partitionKey: BuildRateLimitPartitionKey(context, "auth"),
                    factory: _ => new FixedWindowRateLimiterOptions
                    {
                        PermitLimit = 5,
                        Window = TimeSpan.FromMinutes(1),
                        QueueLimit = 0,
                        AutoReplenishment = true,
                    }));

            options.AddPolicy(ApplicationDefaults.PublicWriteRateLimitPolicy, context =>
                RateLimitPartition.GetFixedWindowLimiter(
                    partitionKey: BuildRateLimitPartitionKey(context, "public-write"),
                    factory: _ => new FixedWindowRateLimiterOptions
                    {
                        PermitLimit = 20,
                        Window = TimeSpan.FromMinutes(1),
                        QueueLimit = 0,
                        AutoReplenishment = true,
                    }));
        });

        services.AddDbContext<AwardsDbContext>(options => options.UseNpgsql(connectionString));

        services.AddScoped<IUserSessionRepository, UserSessionRepository>();
        services.AddScoped<IRiskFlagRepository, RiskFlagRepository>();
        services.AddScoped<IAdminAuditRepository, AdminAuditRepository>();
        services.AddScoped<IUserSessionService, UserSessionService>();
        services.AddScoped<IRiskRuleService, RiskRuleService>();
        services.AddScoped<IRiskFlagService, RiskFlagService>();
        services.AddScoped<IAdminAuditService, AdminAuditService>();
        services.AddScoped<IViewerStatsProvider, TwitchTrackerViewerStatsProvider>();
        services.AddScoped<NominationTrackingReviewService>();
        services.AddScoped<NominationEnrichmentService>();
        services.AddScoped<AdminSessionFilter>();

        return services;
    }

    private static string BuildRateLimitPartitionKey(HttpContext context, string policyName)
    {
        var ipAddress = context.Connection.RemoteIpAddress?.ToString() ?? "unknown-ip";
        var route = context.Request.Path.Value ?? "/";
        return string.Create(
            CultureInfo.InvariantCulture,
            $"{policyName}:{ipAddress}:{route}");
    }

    private static string[] ResolveAllowedOrigins(IConfiguration configuration, IWebHostEnvironment environment)
    {
        var frontendOptions = configuration.GetSection(FrontendOptions.SectionName).Get<FrontendOptions>();
        var configuredOrigins = frontendOptions?.AllowedOrigins
            .Select(NormalizeCorsOrigin)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray() ?? [];

        if (configuredOrigins.Length > 0)
        {
            return configuredOrigins;
        }

        if (environment.IsDevelopment())
        {
            return ApplicationDefaults.FrontendOrigins;
        }

        throw new InvalidOperationException(
            "Frontend:AllowedOrigins must be configured in non-development environments.");
    }

    private static string NormalizeCorsOrigin(string origin)
    {
        var trimmedOrigin = origin.Trim();
        if (string.IsNullOrWhiteSpace(trimmedOrigin) || trimmedOrigin.Contains('*', StringComparison.Ordinal))
        {
            throw new InvalidOperationException("CORS origins must be explicit http(s) origins. Wildcards are not allowed.");
        }

        if (!Uri.TryCreate(trimmedOrigin, UriKind.Absolute, out var uri)
            || uri.Scheme is not ("http" or "https")
            || string.IsNullOrWhiteSpace(uri.Host))
        {
            throw new InvalidOperationException($"Invalid CORS origin configured: {trimmedOrigin}");
        }

        return uri.GetLeftPart(UriPartial.Authority);
    }
}
