using Backend.Common;
using Backend.Contracts;
using Backend.Data;
using Backend.Domain;
using Backend.Services;
using Microsoft.EntityFrameworkCore;

namespace Backend.Endpoints;

public static partial class PublicEndpoints
{
    private static async Task<IResult> CreateNomination(
        HttpContext context,
        CreateNominationRequest request,
        AwardsDbContext db,
        IUserSessionService userSessionService,
        IRiskFlagService riskFlagService,
        IRiskRuleService riskRuleService,
        NominationEnrichmentService nominationEnrichmentService)
    {
        var submittedNominations = NormalizeSubmittedNominations(request);

        if (submittedNominations.Length == 0)
        {
            return Results.BadRequest(new { message = "A nomination request must include at least one stream link." });
        }

        if (submittedNominations.Any(item => item.Name is { Length: > 120 }))
        {
            return Results.BadRequest(new { message = "Legacy nominee names must stay below 120 characters." });
        }

        if (submittedNominations.Any(item => item.StreamUrl.Length > 300))
        {
            return Results.BadRequest(new { message = "Stream links must stay below 300 characters." });
        }

        var distinctStreamUrls = submittedNominations
            .Select(item => NormalizeNominationUrlForCompare(item.StreamUrl))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        if (distinctStreamUrls.Length != submittedNominations.Length)
        {
            return Results.BadRequest(new { message = "Duplicate stream links are not allowed inside one category." });
        }

        var invalidStreamUrl = submittedNominations
            .Where(item => !string.IsNullOrWhiteSpace(item.StreamUrl))
            .Select(item => item.StreamUrl)
            .FirstOrDefault(item => !TryNormalizeExternalUrl(item, out _));

        if (invalidStreamUrl is not null)
        {
            return Results.BadRequest(new { message = "A valid http(s) stream link is required." });
        }

        var settings = await db.SiteSettings.AsNoTracking().FirstOrDefaultAsync(item => item.Id == 1);
        var linkBlacklist = NominationLinkBlacklistSettings.Read(settings);
        var blacklistedStreamUrl = submittedNominations
            .Select(item => item.StreamUrl)
            .FirstOrDefault(item => NominationLinkBlacklistSettings.IsBlocked(item, linkBlacklist));

        if (blacklistedStreamUrl is not null)
        {
            return Results.BadRequest(new { message = "Dieser Link kann nicht nominiert werden. Bitte reiche einen direkten Kanal- oder Profil-Link ein." });
        }

        var season = await db.Seasons.FirstOrDefaultAsync(item => item.Year == request.Year);

        if (season is null)
        {
            return Results.BadRequest(new { message = "The selected season does not exist." });
        }

        var categoryGroupName = await ResolveCategoryGroupNameAsync(db, season.Id, request, context.RequestAborted);
        if (string.IsNullOrWhiteSpace(categoryGroupName))
        {
            return Results.BadRequest(new { message = "The selected category group does not exist for this season." });
        }

        var groupCategories = await db.Categories
            .Where(item => item.SeasonId == season.Id && item.GroupName == categoryGroupName)
            .OrderBy(item => item.SortOrder)
            .ThenBy(item => item.Name)
            .ToArrayAsync(context.RequestAborted);

        if (groupCategories.Length == 0)
        {
            return Results.BadRequest(new { message = "The selected category group does not exist for this season." });
        }

        var maxNomineesPerUser = ResolveMaxNomineesPerUser(groupCategories);
        if (submittedNominations.Length > maxNomineesPerUser)
        {
            return Results.BadRequest(new { message = $"Pro Kategorie sind maximal {maxNomineesPerUser} Links erlaubt." });
        }

        var nominationSeasonResolution = EnsurePublicWriteSeason(season, "nomination");
        if (nominationSeasonResolution.Result is not null)
        {
            return nominationSeasonResolution.Result;
        }

        var submitterIdResult = await ResolveSubmitterIdAsync(context, request.TwitchUserId, userSessionService);
        if (submitterIdResult.Result is not null)
        {
            return submitterIdResult.Result;
        }

        var submitterId = submitterIdResult.SubmitterId!;
        var requestMetadata = RequestMetadataReader.Read(context);
        var existingNominationCount = await db.Nominations.CountAsync(item =>
            item.SeasonId == season.Id
            && item.CategoryGroupName == categoryGroupName
            && item.SubmittedByTwitchId == submitterId
            && item.Status == "pending");

        var records = submittedNominations.Select(nomination => new Nomination
        {
            SeasonId = season.Id,
            CategoryId = null,
            CategoryGroupName = categoryGroupName,
            SubmittedByTwitchId = submitterId,
            CandidateText = string.IsNullOrWhiteSpace(nomination.Name) ? null : nomination.Name,
            StreamUrl = string.IsNullOrWhiteSpace(nomination.StreamUrl) ? null : nomination.StreamUrl,
            Status = "pending",
            ReviewNote = $"Stream-Link: {nomination.StreamUrl}",
            CreatedAt = DateTimeOffset.UtcNow,
        }).ToArray();

        foreach (var record in records)
        {
            await nominationEnrichmentService.EnrichAsync(record, groupCategories, context.RequestAborted);
        }

        await db.Nominations.AddRangeAsync(records);

        var resubmittedNominationRule = await riskRuleService.GetRuleAsync("resubmitted_nomination", context.RequestAborted);
        var rapidNominationBurstRule = await riskRuleService.GetRuleAsync("rapid_nomination_burst", context.RequestAborted);
        var recentNominationVolume = await db.Nominations.CountAsync(item =>
            item.SubmittedByTwitchId == submitterId
            && item.CreatedAt >= DateTimeOffset.UtcNow.AddMinutes(-rapidNominationBurstRule.WindowMinutes));

        await db.SaveChangesAsync(context.RequestAborted);
        var reviewLink = new
        {
            label = "Review-Fälle öffnen",
            entityType = "nomination",
            entityId = string.Join(",", records.Select(item => item.Id)),
            to = $"/admin/reviews?query={Uri.EscapeDataString(submitterId)}",
        };

        if (existingNominationCount > 0 && resubmittedNominationRule.Enabled)
        {
            await riskFlagService.AddIfMissingAsync(
                season.Id,
                submitterId,
                "nomination",
                "resubmitted_nomination",
                resubmittedNominationRule.Severity,
                "Ein User hat seine Nominierung in derselben Hauptkategorie erneut eingereicht.",
                requestMetadata,
                new { categoryGroupName, existingNominationCount, nominationIds = records.Select(item => item.Id).ToArray(), entityLinks = new[] { reviewLink } },
                context.RequestAborted);
        }

        if (rapidNominationBurstRule.Enabled && recentNominationVolume >= rapidNominationBurstRule.Threshold)
        {
            await riskFlagService.AddIfMissingAsync(
                season.Id,
                submitterId,
                "nomination",
                "rapid_nomination_burst",
                rapidNominationBurstRule.Severity,
                "Ungewoehnlich viele Nominierungsaktionen in kurzer Zeit erkannt.",
                requestMetadata,
                new { recentNominationVolume, threshold = rapidNominationBurstRule.Threshold, windowMinutes = rapidNominationBurstRule.WindowMinutes, nominationIds = records.Select(item => item.Id).ToArray(), entityLinks = new[] { reviewLink } },
                context.RequestAborted);
        }

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { saved = submittedNominations.Length, categoryGroupName, collectedSignal = existingNominationCount > 0 });
    }

    private readonly record struct SubmittedNomination(string? Name, string StreamUrl);

    private static SubmittedNomination[] NormalizeSubmittedNominations(CreateNominationRequest request)
    {
        if (request.Nominations is { Length: > 0 })
        {
            return request.Nominations
                .Select(item =>
                {
                    var name = item.Name?.Trim() ?? string.Empty;
                    var streamUrl = item.StreamUrl?.Trim() ?? string.Empty;
                    if (!string.IsNullOrWhiteSpace(streamUrl) && TryNormalizeExternalUrl(streamUrl, out var normalizedUrl))
                    {
                        streamUrl = normalizedUrl;
                    }

                    return new SubmittedNomination(string.IsNullOrWhiteSpace(name) ? null : name, streamUrl);
                })
                .Where(item => !string.IsNullOrWhiteSpace(item.StreamUrl))
                .ToArray();
        }

        return (request.Nominees ?? [])
            .Select(item =>
            {
                var streamUrl = item.Trim();
                if (!string.IsNullOrWhiteSpace(streamUrl) && TryNormalizeExternalUrl(streamUrl, out var normalizedUrl))
                {
                    streamUrl = normalizedUrl;
                }

                return new SubmittedNomination(null, streamUrl);
            })
            .Where(item => !string.IsNullOrWhiteSpace(item.StreamUrl))
            .ToArray();
    }

    private static string NormalizeNominationUrlForCompare(string value) =>
        value.Trim().TrimEnd('/').ToLowerInvariant();

    private static int ResolveMaxNomineesPerUser(IEnumerable<Category> groupCategories)
    {
        var configuredLimit = groupCategories
            .Select(item => item.MaxNomineesPerUser)
            .Where(value => value > 0)
            .DefaultIfEmpty(3)
            .Max();

        return Math.Clamp(configuredLimit, 1, 10);
    }

    private static async Task<string?> ResolveCategoryGroupNameAsync(
        AwardsDbContext db,
        int seasonId,
        CreateNominationRequest request,
        CancellationToken cancellationToken)
    {
        var categoryGroupName = request.CategoryGroupName?.Trim();
        if (!string.IsNullOrWhiteSpace(categoryGroupName))
        {
            return await db.Categories
                .Where(item => item.SeasonId == seasonId && item.GroupName == categoryGroupName)
                .Select(item => item.GroupName)
                .FirstOrDefaultAsync(cancellationToken);
        }

        if (!request.CategoryId.HasValue)
        {
            return null;
        }

        return await db.Categories
            .Where(item => item.SeasonId == seasonId && item.Id == request.CategoryId.Value)
            .Select(item => item.GroupName)
            .FirstOrDefaultAsync(cancellationToken);
    }
}
