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

namespace Backend.Endpoints;

public static partial class AdminSeasonManagementEndpoints
{
    private static async Task<IResult> UpdateSeasonSubcategoryTemplates(
        HttpContext context,
        int seasonId,
        UpdateSeasonSubcategoryTemplatesRequest request,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var validationError = ValidateSubcategoryTemplatesRequest(request);
        if (validationError is not null)
        {
            return validationError;
        }

        var session = AdminEndpointConventions.CurrentSession(context);
        var season = await db.Seasons.FirstOrDefaultAsync(item => item.Id == seasonId, context.RequestAborted);
        if (season is null)
        {
            return Results.NotFound();
        }

        var categories = await db.Categories
            .Where(item => item.SeasonId == seasonId)
            .OrderBy(item => item.SortOrder)
            .ThenBy(item => item.Name)
            .ToListAsync(context.RequestAborted);
        var templates = SeasonSubcategoryTemplateSettings.Normalize(request.Templates);
        var blockedRemovals = await FindBlockedSubcategoryRemovalsAsync(db, categories, templates, context.RequestAborted);
        if (blockedRemovals.Length > 0)
        {
            var firstBlocked = blockedRemovals[0];
            return Results.BadRequest(new
            {
                message = $"Unterkategorie \"{firstBlocked.SubcategoryName}\" kann nicht entfernt werden, weil darunter noch {firstBlocked.CandidateCount} Kandidaten und {firstBlocked.NominationCount} Nominierungen haengen.",
                blockedSubcategories = blockedRemovals,
            });
        }

        season.SubcategoryTemplatesJson = SeasonSubcategoryTemplateSettings.Serialize(templates);
        SyncCategoryGroupsToTemplates(db, season, categories, templates);

        adminAuditService.AddEntry(
            session.TwitchUserId,
            "category-templates.update",
            "season",
            seasonId.ToString(),
            $"Unterkategorien für {season.Year} wurden aktualisiert.",
            new { seasonId, templateCount = templates.Length },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { saved = true, seasonId, templateCount = templates.Length });
    }

    private static async Task<IResult> CreateCategoryGroup(
        HttpContext context,
        int seasonId,
        UpsertCategoryGroupRequest request,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var validationError = ValidateCategoryGroupRequest(request);
        if (validationError is not null)
        {
            return validationError;
        }

        var session = AdminEndpointConventions.CurrentSession(context);
        var season = await db.Seasons.FirstOrDefaultAsync(item => item.Id == seasonId, context.RequestAborted);
        if (season is null)
        {
            return Results.NotFound();
        }

        var categories = await db.Categories
            .Where(item => item.SeasonId == seasonId)
            .OrderBy(item => item.SortOrder)
            .ThenBy(item => item.Name)
            .ToListAsync(context.RequestAborted);
        var templates = SeasonSubcategoryTemplateSettings.Read(season, categories);
        if (templates.Length == 0)
        {
            return Results.BadRequest(new { message = "Lege zuerst mindestens eine globale Unterkategorie an." });
        }

        var groupName = request.GroupName.Trim();
        if (categories.Any(item => string.Equals(item.GroupName, groupName, StringComparison.OrdinalIgnoreCase)))
        {
            return Results.BadRequest(new { message = "Eine Hauptkategorie mit diesem Namen existiert bereits in dieser Season." });
        }

        for (var index = 0; index < templates.Length; index += 1)
        {
            categories.Add(new Category
            {
                SeasonId = seasonId,
                GroupName = groupName,
                Name = templates[index].Name,
                Slug = BuildCategorySlug(groupName, templates[index].Slug),
                Description = request.Description.Trim(),
                SortOrder = request.SortOrder,
                MaxNomineesPerUser = request.MaxNomineesPerUser,
                ViewerRangeMin = templates[index].ViewerRangeMin,
                ViewerRangeMax = templates[index].ViewerRangeMax,
            });
        }

        foreach (var category in categories.Where(item => item.Id == 0))
        {
            db.Categories.Add(category);
        }

        SyncCategoryGroupsToTemplates(db, season, categories, templates);

        adminAuditService.AddEntry(
            session.TwitchUserId,
            "category-group.create",
            "season",
            seasonId.ToString(),
            $"Hauptkategorie {groupName} wurde angelegt.",
            new { seasonId, groupName, request.SortOrder, request.MaxNomineesPerUser },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { saved = true, seasonId, groupName });
    }

    private static async Task<IResult> UpdateCategoryGroup(
        HttpContext context,
        int seasonId,
        string groupName,
        UpsertCategoryGroupRequest request,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var validationError = ValidateCategoryGroupRequest(request);
        if (validationError is not null)
        {
            return validationError;
        }

        var session = AdminEndpointConventions.CurrentSession(context);
        var season = await db.Seasons.FirstOrDefaultAsync(item => item.Id == seasonId, context.RequestAborted);
        if (season is null)
        {
            return Results.NotFound();
        }

        var categories = await db.Categories
            .Where(item => item.SeasonId == seasonId)
            .OrderBy(item => item.SortOrder)
            .ThenBy(item => item.Name)
            .ToListAsync(context.RequestAborted);
        var templates = SeasonSubcategoryTemplateSettings.Read(season, categories);
        var normalizedCurrentName = groupName.Trim();
        var groupCategories = categories
            .Where(item => string.Equals(item.GroupName, normalizedCurrentName, StringComparison.OrdinalIgnoreCase))
            .ToArray();
        if (groupCategories.Length == 0)
        {
            return Results.NotFound();
        }

        var targetName = request.GroupName.Trim();
        if (!string.Equals(normalizedCurrentName, targetName, StringComparison.OrdinalIgnoreCase)
            && categories.Any(item => string.Equals(item.GroupName, targetName, StringComparison.OrdinalIgnoreCase)))
        {
            return Results.BadRequest(new { message = "Eine Hauptkategorie mit diesem Namen existiert bereits in dieser Season." });
        }

        foreach (var category in groupCategories)
        {
            category.GroupName = targetName;
            category.Description = request.Description.Trim();
            category.SortOrder = request.SortOrder;
            category.MaxNomineesPerUser = request.MaxNomineesPerUser;
        }

        SyncCategoryGroupsToTemplates(db, season, categories, templates);

        adminAuditService.AddEntry(
            session.TwitchUserId,
            "category-group.update",
            "season",
            seasonId.ToString(),
            $"Hauptkategorie {normalizedCurrentName} wurde aktualisiert.",
            new { seasonId, from = normalizedCurrentName, to = targetName, request.SortOrder, request.MaxNomineesPerUser },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { saved = true, seasonId, groupName = targetName });
    }

    private static async Task<IResult> DeleteCategoryGroup(
        HttpContext context,
        int seasonId,
        string groupName,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);
        var normalizedGroupName = groupName.Trim();
        var categories = await db.Categories
            .Where(item => item.SeasonId == seasonId && item.GroupName.ToLower() == normalizedGroupName.ToLower())
            .ToListAsync(context.RequestAborted);
        if (categories.Count == 0)
        {
            return Results.NotFound();
        }

        var categoryIds = categories.Select(item => item.Id).ToArray();
        var candidates = await db.Candidates
            .Where(item => categoryIds.Contains(item.CategoryId))
            .ToArrayAsync(context.RequestAborted);

        if (candidates.Length > 0)
        {
            db.Candidates.RemoveRange(candidates);
        }

        db.Categories.RemoveRange(categories);

        adminAuditService.AddEntry(
            session.TwitchUserId,
            "category-group.delete",
            "season",
            seasonId.ToString(),
            $"Hauptkategorie {normalizedGroupName} wurde gelöscht.",
            new { seasonId, removedCategories = categories.Count, removedCandidates = candidates.Length },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { deleted = true, seasonId, groupName = normalizedGroupName });
    }

    private static void SyncCategoryGroupsToTemplates(
        AwardsDbContext db,
        Season season,
        List<Category> categories,
        SeasonSubcategoryTemplateSetting[] templates)
    {
        var orderedGroups = categories
            .Where(item => !string.IsNullOrWhiteSpace(item.GroupName))
            .GroupBy(item => item.GroupName.Trim(), StringComparer.OrdinalIgnoreCase)
            .Select(group =>
            {
                var items = group.OrderBy(item => item.SortOrder).ThenBy(item => item.Name).ToList();
                var sample = items[0];
                return new
                {
                    GroupName = sample.GroupName.Trim(),
                    Description = sample.Description.Trim(),
                    SortOrder = items.Min(item => item.SortOrder),
                    MaxNomineesPerUser = sample.MaxNomineesPerUser,
                    Items = items,
                };
            })
            .OrderBy(group => group.SortOrder)
            .ThenBy(group => group.GroupName, StringComparer.OrdinalIgnoreCase)
            .ToArray();

        var nextSortOrder = 1;
        foreach (var group in orderedGroups)
        {
            var usedCategories = new HashSet<Category>();
            for (var index = 0; index < templates.Length; index += 1)
            {
                var template = templates[index];
                var category = FindReusableCategoryForTemplate(group.Items, template, group.GroupName, usedCategories)
                    ?? new Category { SeasonId = season.Id };
                usedCategories.Add(category);

                category.GroupName = group.GroupName;
                category.Name = template.Name;
                category.Slug = BuildCategorySlug(group.GroupName, template.Slug);
                category.Description = group.Description;
                category.MaxNomineesPerUser = group.MaxNomineesPerUser;
                category.ViewerRangeMin = template.ViewerRangeMin;
                category.ViewerRangeMax = template.ViewerRangeMax;
                category.SortOrder = nextSortOrder++;

                if (category.Id == 0 && !db.Categories.Local.Any(item => ReferenceEquals(item, category)))
                {
                    db.Categories.Add(category);
                    categories.Add(category);
                }
            }

            foreach (var staleCategory in FindStaleCategoriesAfterTemplateSync(group.Items, templates, group.GroupName))
            {
                RemoveCategoryWithCandidates(db, staleCategory);
                categories.Remove(staleCategory);
            }
        }
    }

    private static void RemoveCategoryWithCandidates(AwardsDbContext db, Category category)
    {
        if (category.Id > 0)
        {
            var candidates = db.Candidates.Where(item => item.CategoryId == category.Id).ToArray();
            if (candidates.Length > 0)
            {
                db.Candidates.RemoveRange(candidates);
            }
        }

        db.Categories.Remove(category);
    }

    private static async Task<BlockedSubcategoryRemoval[]> FindBlockedSubcategoryRemovalsAsync(
        AwardsDbContext db,
        List<Category> categories,
        SeasonSubcategoryTemplateSetting[] templates,
        CancellationToken cancellationToken)
    {
        var staleCategories = categories
            .Where(item => !string.IsNullOrWhiteSpace(item.GroupName))
            .GroupBy(item => item.GroupName.Trim(), StringComparer.OrdinalIgnoreCase)
            .SelectMany(group => FindStaleCategoriesAfterTemplateSync(
                group.OrderBy(item => item.SortOrder).ThenBy(item => item.Name).ToList(),
                templates,
                group.Key))
            .Where(item => item.Id > 0)
            .ToArray();

        if (staleCategories.Length == 0)
        {
            return [];
        }

        var staleCategoryIds = staleCategories.Select(item => item.Id).ToArray();
        var candidateCounts = await db.Candidates
            .Where(item => staleCategoryIds.Contains(item.CategoryId))
            .GroupBy(item => item.CategoryId)
            .Select(group => new { CategoryId = group.Key, Count = group.Count() })
            .ToDictionaryAsync(item => item.CategoryId, item => item.Count, cancellationToken);
        var nominationCounts = await db.Nominations
            .Where(item =>
                (item.CategoryId != null && staleCategoryIds.Contains(item.CategoryId.Value))
                || (item.SuggestedCategoryId != null && staleCategoryIds.Contains(item.SuggestedCategoryId.Value)))
            .GroupBy(item => item.CategoryId != null && staleCategoryIds.Contains(item.CategoryId.Value)
                ? item.CategoryId!.Value
                : item.SuggestedCategoryId!.Value)
            .Select(group => new { CategoryId = group.Key, Count = group.Count() })
            .ToDictionaryAsync(item => item.CategoryId, item => item.Count, cancellationToken);

        return staleCategories
            .Select(category => new BlockedSubcategoryRemoval(
                category.GroupName,
                category.Name,
                category.Slug,
                candidateCounts.GetValueOrDefault(category.Id),
                nominationCounts.GetValueOrDefault(category.Id)))
            .Where(item => item.CandidateCount > 0 || item.NominationCount > 0)
            .OrderBy(item => item.GroupName, StringComparer.OrdinalIgnoreCase)
            .ThenBy(item => item.SubcategoryName, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static Category? FindReusableCategoryForTemplate(
        List<Category> categories,
        SeasonSubcategoryTemplateSetting template,
        string groupName,
        HashSet<Category>? usedCategories = null)
    {
        usedCategories ??= [];
        var expectedSlug = BuildCategorySlug(groupName, template.Slug);
        var normalizedTemplateSlug = SeasonSubcategoryTemplateSettings.Slugify(template.Slug);

        return categories.FirstOrDefault(item => !usedCategories.Contains(item)
                && string.Equals(item.Slug, expectedSlug, StringComparison.OrdinalIgnoreCase))
            ?? categories.FirstOrDefault(item => !usedCategories.Contains(item)
                && string.Equals(item.Slug, normalizedTemplateSlug, StringComparison.OrdinalIgnoreCase))
            ?? categories.FirstOrDefault(item => !usedCategories.Contains(item)
                && item.Slug.EndsWith($"-{normalizedTemplateSlug}", StringComparison.OrdinalIgnoreCase))
            ?? categories.FirstOrDefault(item => !usedCategories.Contains(item)
                && string.Equals(item.Name, template.Name, StringComparison.OrdinalIgnoreCase));
    }

    private static Category[] FindStaleCategoriesAfterTemplateSync(
        List<Category> categories,
        SeasonSubcategoryTemplateSetting[] templates,
        string groupName)
    {
        var usedCategories = new HashSet<Category>();
        foreach (var template in templates)
        {
            var reusableCategory = FindReusableCategoryForTemplate(categories, template, groupName, usedCategories);
            if (reusableCategory is not null)
            {
                usedCategories.Add(reusableCategory);
            }
        }

        return categories
            .Where(item => !usedCategories.Contains(item))
            .ToArray();
    }

    private static IResult? ValidateSubcategoryTemplatesRequest(UpdateSeasonSubcategoryTemplatesRequest request)
    {
        var templates = SeasonSubcategoryTemplateSettings.Normalize(request.Templates);
        if (templates.Length == 0)
        {
            return Results.BadRequest(new { message = "Mindestens eine Unterkategorie ist erforderlich." });
        }

        var duplicateNames = templates
            .GroupBy(item => item.Name, StringComparer.OrdinalIgnoreCase)
            .Any(group => group.Count() > 1);
        if (duplicateNames)
        {
            return Results.BadRequest(new { message = "Unterkategorien dürfen nicht denselben Namen mehrfach verwenden." });
        }

        var duplicateSlugs = templates
            .GroupBy(item => item.Slug, StringComparer.OrdinalIgnoreCase)
            .Any(group => group.Count() > 1);
        if (duplicateSlugs)
        {
            return Results.BadRequest(new { message = "Unterkategorien dürfen nicht denselben Slug mehrfach verwenden." });
        }

        foreach (var template in templates)
        {
            if (string.IsNullOrWhiteSpace(template.Name) || template.Name.Length > 120)
            {
                return Results.BadRequest(new { message = "Unterkategorie-Name ist erforderlich und muss unter 120 Zeichen bleiben." });
            }

            if (string.IsNullOrWhiteSpace(template.Slug) || template.Slug.Length > 120)
            {
                return Results.BadRequest(new { message = "Unterkategorie-Slug ist erforderlich und muss unter 120 Zeichen bleiben." });
            }

            if (template.ViewerRangeMax is not null
                && template.ViewerRangeMin is not null
                && template.ViewerRangeMax < template.ViewerRangeMin)
            {
                return Results.BadRequest(new { message = "Viewer-Range Ende muss groesser oder gleich dem Start sein." });
            }
        }

        return null;
    }

    private static IResult? ValidateCategoryGroupRequest(UpsertCategoryGroupRequest request)
    {
        var groupName = request.GroupName.Trim();
        var description = request.Description.Trim();

        if (string.IsNullOrWhiteSpace(groupName) || groupName.Length > 80)
        {
            return Results.BadRequest(new { message = "Hauptkategorie ist erforderlich und muss unter 80 Zeichen bleiben." });
        }

        if (description.Length > 400)
        {
            return Results.BadRequest(new { message = "Beschreibung muss unter 400 Zeichen bleiben." });
        }

        if (request.SortOrder is < 1 or > 200)
        {
            return Results.BadRequest(new { message = "Die Reihenfolge muss zwischen 1 und 200 liegen." });
        }

        if (request.MaxNomineesPerUser is < 1 or > 10)
        {
            return Results.BadRequest(new { message = "Das Nominierungs-Limit muss zwischen 1 und 10 liegen." });
        }

        return null;
    }

    private sealed record BlockedSubcategoryRemoval(
        string GroupName,
        string SubcategoryName,
        string Slug,
        int CandidateCount,
        int NominationCount);

    private static string BuildCategorySlug(string groupName, string templateSlug)
    {
        var groupSlug = SeasonSubcategoryTemplateSettings.Slugify(groupName);
        var detailSlug = SeasonSubcategoryTemplateSettings.Slugify(templateSlug);
        return string.IsNullOrWhiteSpace(groupSlug) ? detailSlug : $"{groupSlug}-{detailSlug}";
    }
}
