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 AdminModerationEndpoints
{
    private static async Task<IResult> ApproveNomination(
        HttpContext context,
        int nominationId,
        ApproveNominationRequest request,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);
        var nomination = await db.Nominations
            .Include(item => item.Category)
            .Include(item => item.SuggestedCategory)
            .Include(item => item.StreamerIdentity)
            .FirstOrDefaultAsync(item => item.Id == nominationId);

        if (nomination is null)
        {
            return Results.NotFound();
        }

        var rawDisplayName = FirstNonEmpty(request.DisplayName, nomination.CandidateText, nomination.ResolvedChannel);

        if (string.IsNullOrWhiteSpace(rawDisplayName))
        {
            return Results.BadRequest(new { message = "A display name is required to approve the nomination." });
        }

        var categoryId = request.CategoryId ?? nomination.SuggestedCategoryId;
        if (!categoryId.HasValue)
        {
            return Results.BadRequest(new { message = "Bitte waehle ein Tier aus. Fuer diesen Link konnte kein automatischer Vorschlag ermittelt werden." });
        }

        var targetCategory = await db.Categories.FirstOrDefaultAsync(item =>
            item.Id == categoryId.Value
            && item.SeasonId == nomination.SeasonId
            && item.GroupName == nomination.CategoryGroupName);
        if (targetCategory is null)
        {
            return Results.BadRequest(new { message = "Das gewaehlte Tier gehoert nicht zur Hauptkategorie dieser Nominierung." });
        }

        var channelSlug = FirstNonEmpty(request.ChannelSlug, nomination.ResolvedChannel);
        var platform = string.IsNullOrWhiteSpace(request.Platform)
            ? nomination.ResolvedPlatform?.Trim() ?? "Twitch"
            : request.Platform.Trim();
        var normalizedDisplayName = rawDisplayName.ToLower();
        var normalizedChannelSlug = channelSlug.ToLower();
        var normalizedPlatform = platform.ToLower();

        var existingCandidate = await db.Candidates.FirstOrDefaultAsync(item =>
            item.SeasonId == nomination.SeasonId
            && item.CategoryId == targetCategory.Id
            && (
                (nomination.StreamerIdentityId != null && item.StreamerIdentityId == nomination.StreamerIdentityId)
                ||
                item.DisplayName.ToLower() == normalizedDisplayName
                || (!string.IsNullOrWhiteSpace(normalizedChannelSlug)
                    && item.ChannelSlug.ToLower() == normalizedChannelSlug
                    && item.Platform.ToLower() == normalizedPlatform)
            ));

        var workflowRuleBlock = await BuildModerationCandidateWorkflowRuleBlockAsync(
            db,
            nomination.SeasonId,
            targetCategory.Id,
            existingCandidate?.Id,
            nomination.StreamerIdentityId,
            rawDisplayName,
            channelSlug,
            existingCandidate?.AcceptanceStatus ?? "open",
            context.RequestAborted);
        if (workflowRuleBlock is not null)
        {
            return workflowRuleBlock;
        }

        var candidate = existingCandidate;
        if (candidate is null)
        {
            candidate = new Candidate
            {
                SeasonId = nomination.SeasonId,
                CategoryId = targetCategory.Id,
                StreamerIdentityId = nomination.StreamerIdentityId,
                DisplayName = rawDisplayName,
                ChannelSlug = channelSlug,
                Platform = platform,
            };

            db.Candidates.Add(candidate);
            await db.SaveChangesAsync(context.RequestAborted);
        }
        else
        {
            candidate.StreamerIdentityId ??= nomination.StreamerIdentityId;
            candidate.DisplayName = rawDisplayName;

            if (!string.IsNullOrWhiteSpace(channelSlug))
            {
                candidate.ChannelSlug = channelSlug;
            }

            if (!string.IsNullOrWhiteSpace(platform))
            {
                candidate.Platform = platform;
            }
        }

        var relatedNominations = await FindRelatedPendingNominationsAsync(db, nomination, context.RequestAborted);
        var uniqueViewerCount = relatedNominations
            .Select(item => item.SubmittedByTwitchId.Trim().ToLowerInvariant())
            .Where(item => !string.IsNullOrWhiteSpace(item))
            .Distinct()
            .Count();

        candidate.NominationTally = Math.Max(candidate.NominationTally, uniqueViewerCount);

        foreach (var relatedNomination in relatedNominations)
        {
            relatedNomination.CandidateId = candidate.Id;
            relatedNomination.SuggestedCategoryId ??= targetCategory.Id;
            relatedNomination.Status = "approved";
            relatedNomination.ReviewNote = string.IsNullOrWhiteSpace(request.ReviewNote) ? null : request.ReviewNote.Trim();
            relatedNomination.ReviewedAt = DateTimeOffset.UtcNow;
            relatedNomination.ReviewedByTwitchId = session.TwitchUserId;
            ApplyTrackingReviewDecision(relatedNomination, request.ReviewNote, session.TwitchUserId);
        }

        adminAuditService.AddEntry(
            session.TwitchUserId,
            "nomination.approve",
            "nomination",
            nomination.Id.ToString(),
            $"Nominierung {nomination.Id} wurde als Kandidat uebernommen. {uniqueViewerCount} Viewer haben diesen Streamer nominiert.",
            new
            {
                candidateId = candidate.Id,
                created = existingCandidate is null,
                targetCategoryId = targetCategory.Id,
                targetCategoryName = targetCategory.Name,
                nominationIds = relatedNominations.Select(item => item.Id).ToArray(),
                uniqueViewerCount,
                reviewNote = string.IsNullOrWhiteSpace(request.ReviewNote) ? null : request.ReviewNote.Trim(),
            },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new
        {
            saved = true,
            nominationId = nomination.Id,
            candidateId = candidate.Id,
            created = existingCandidate is null,
            uniqueViewerCount,
            nominationIds = relatedNominations.Select(item => item.Id).ToArray(),
        });
    }

    private static async Task<IResult> RejectNomination(
        HttpContext context,
        int nominationId,
        RejectNominationRequest request,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);
        var nomination = await db.Nominations.FirstOrDefaultAsync(item => item.Id == nominationId);
        if (nomination is null)
        {
            return Results.NotFound();
        }

        var relatedNominations = await FindRelatedPendingNominationsAsync(db, nomination, context.RequestAborted);
        foreach (var relatedNomination in relatedNominations)
        {
            relatedNomination.CandidateId = null;
            relatedNomination.Status = "rejected";
            relatedNomination.ReviewNote = string.IsNullOrWhiteSpace(request.ReviewNote) ? null : request.ReviewNote.Trim();
            relatedNomination.ReviewedAt = DateTimeOffset.UtcNow;
            relatedNomination.ReviewedByTwitchId = session.TwitchUserId;
            ApplyTrackingReviewDecision(relatedNomination, request.ReviewNote, session.TwitchUserId);
        }

        adminAuditService.AddEntry(
            session.TwitchUserId,
            "nomination.reject",
            "nomination",
            nomination.Id.ToString(),
            $"Nominierung {nomination.Id} wurde verworfen.",
            new
            {
                reviewNote = string.IsNullOrWhiteSpace(request.ReviewNote) ? null : request.ReviewNote.Trim(),
                nominationIds = relatedNominations.Select(item => item.Id).ToArray(),
            },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { saved = true, nominationId = nomination.Id, rejected = true, nominationIds = relatedNominations.Select(item => item.Id).ToArray() });
    }

    private static async Task<IResult> ReopenRejectedNomination(
        HttpContext context,
        int nominationId,
        ReopenRejectedNominationRequest request,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);
        var nomination = await db.Nominations.FirstOrDefaultAsync(item => item.Id == nominationId, context.RequestAborted);
        if (nomination is null)
        {
            return Results.NotFound();
        }

        if (!string.Equals(nomination.Status, "rejected", StringComparison.OrdinalIgnoreCase))
        {
            return Results.BadRequest(new { message = "Nur verworfene Nominierungen koennen wieder geoeffnet werden." });
        }

        var relatedNominations = await FindRelatedNominationsByStatusAsync(db, nomination, "rejected", context.RequestAborted);
        foreach (var relatedNomination in relatedNominations)
        {
            relatedNomination.CandidateId = null;
            relatedNomination.Status = "pending";
            relatedNomination.ReviewNote = null;
            relatedNomination.ReviewedAt = null;
            relatedNomination.ReviewedByTwitchId = null;
        }

        var reviewNote = string.IsNullOrWhiteSpace(request.ReviewNote) ? null : request.ReviewNote.Trim();
        adminAuditService.AddEntry(
            session.TwitchUserId,
            "nomination.reopen",
            "nomination",
            nomination.Id.ToString(),
            $"Nominierung {nomination.Id} wurde wieder in die Review-Queue gelegt.",
            new
            {
                reviewNote,
                nominationIds = relatedNominations.Select(item => item.Id).ToArray(),
            },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { saved = true, nominationId = nomination.Id, reopened = true, nominationIds = relatedNominations.Select(item => item.Id).ToArray() });
    }

    private static async Task<IResult> UpdateNominationTrackingReview(
        HttpContext context,
        int nominationId,
        UpdateNominationTrackingReviewRequest request,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);
        var nomination = await db.Nominations.FirstOrDefaultAsync(item => item.Id == nominationId, context.RequestAborted);
        if (nomination is null)
        {
            return Results.NotFound();
        }

        var normalizedStatus = request.Status?.Trim().ToLowerInvariant();
        if (normalizedStatus is not ("reviewed" or "overridden"))
        {
            return Results.BadRequest(new { message = "Tracking-Review-Status muss reviewed oder overridden sein." });
        }

        var relatedNominations = await FindRelatedPendingNominationsAsync(db, nomination, context.RequestAborted);
        var reviewNote = string.IsNullOrWhiteSpace(request.ReviewNote) ? null : request.ReviewNote.Trim();
        var requiresOverrideNote = relatedNominations
            .SelectMany(item => TrackingRulesSettings.ReadFlagHits(item.TrackingFlagsJson))
            .Any(flag => flag.AdminNoteRequiredOnOverride);

        if (normalizedStatus == "overridden" && requiresOverrideNote && string.IsNullOrWhiteSpace(reviewNote))
        {
            return Results.BadRequest(new { message = "Fuer diesen Override ist eine Tracking-Review-Notiz Pflicht." });
        }

        foreach (var item in relatedNominations)
        {
            item.TrackingReviewStatus = normalizedStatus;
            item.TrackingReviewNote = reviewNote;
            item.TrackingReviewedByTwitchId = session.TwitchUserId;
            item.TrackingReviewedAt = DateTimeOffset.UtcNow;
        }

        adminAuditService.AddEntry(
            session.TwitchUserId,
            "nomination.tracking-review.update",
            "nomination",
            nomination.Id.ToString(),
            $"Tracking-Review fuer Nominierung {nomination.Id} wurde auf {normalizedStatus} gesetzt.",
            new
            {
                nominationIds = relatedNominations.Select(item => item.Id).ToArray(),
                status = normalizedStatus,
                reviewNote,
            },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { saved = true, nominationId, status = normalizedStatus });
    }

    private static async Task<Nomination[]> FindRelatedPendingNominationsAsync(
        AwardsDbContext db,
        Nomination nomination,
        CancellationToken cancellationToken)
        => await FindRelatedNominationsByStatusAsync(db, nomination, "pending", cancellationToken);

    private static async Task<Nomination[]> FindRelatedNominationsByStatusAsync(
        AwardsDbContext db,
        Nomination nomination,
        string status,
        CancellationToken cancellationToken)
    {
        var query = db.Nominations
            .Where(item =>
                item.SeasonId == nomination.SeasonId
                && item.CategoryGroupName == nomination.CategoryGroupName
                && item.Status == status);

        if (nomination.StreamerIdentityId.HasValue)
        {
            return await query
                .Where(item => item.StreamerIdentityId == nomination.StreamerIdentityId)
                .ToArrayAsync(cancellationToken);
        }

        var normalizedStreamUrl = NormalizeModerationStreamUrl(nomination.StreamUrl);
        if (!string.IsNullOrWhiteSpace(normalizedStreamUrl))
        {
            var rows = await query.ToArrayAsync(cancellationToken);
            return rows
                .Where(item => string.Equals(NormalizeModerationStreamUrl(item.StreamUrl), normalizedStreamUrl, StringComparison.OrdinalIgnoreCase))
                .ToArray();
        }

        return [nomination];
    }

    private static string NormalizeModerationStreamUrl(string? value) =>
        (value ?? string.Empty).Trim().TrimEnd('/').ToLowerInvariant();

    private static string FirstNonEmpty(params string?[] values) =>
        values
            .Select(value => value?.Trim() ?? string.Empty)
            .FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))
        ?? string.Empty;

    private static async Task<IResult?> BuildModerationCandidateWorkflowRuleBlockAsync(
        AwardsDbContext db,
        int seasonId,
        int categoryId,
        int? existingCandidateId,
        int? streamerIdentityId,
        string displayName,
        string channelSlug,
        string acceptanceStatus,
        CancellationToken cancellationToken)
    {
        if (string.Equals(acceptanceStatus, "declined", StringComparison.OrdinalIgnoreCase))
        {
            return null;
        }

        var season = await db.Seasons
            .AsNoTracking()
            .FirstOrDefaultAsync(item => item.Id == seasonId, cancellationToken);
        var settings = await db.SiteSettings.AsNoTracking().FirstOrDefaultAsync(item => item.Id == 1, cancellationToken);
        var rules = WorkflowRuleSettings.Read(season, settings);
        var finalistsRule = WorkflowRuleSettings.Find(rules, WorkflowRuleSettings.MaxFinalistsPerCategory);
        var appearancesRule = WorkflowRuleSettings.Find(rules, WorkflowRuleSettings.MaxCandidateAppearances);
        if (!WorkflowRuleSettings.ShouldBlock(finalistsRule) && !WorkflowRuleSettings.ShouldBlock(appearancesRule))
        {
            return null;
        }

        var existingCandidates = await db.Candidates
            .AsNoTracking()
            .Where(item =>
                item.SeasonId == seasonId
                && (!existingCandidateId.HasValue || item.Id != existingCandidateId.Value)
                && item.AcceptanceStatus != "declined")
            .Select(item => new
            {
                item.CategoryId,
                item.StreamerIdentityId,
                item.DisplayName,
                item.ChannelSlug,
            })
            .ToArrayAsync(cancellationToken);

        if (WorkflowRuleSettings.ShouldBlock(finalistsRule))
        {
            var categoryCount = existingCandidates.Count(item => item.CategoryId == categoryId);
            if (categoryCount >= finalistsRule.Limit)
            {
                return CreateModerationWorkflowRuleError(
                    $"In dieser Kategorie sind bereits {categoryCount} von {finalistsRule.Limit} finalen Kandidat:innen angelegt.");
            }
        }

        if (WorkflowRuleSettings.ShouldBlock(appearancesRule))
        {
            var identityKey = WorkflowRuleSettings.CandidateIdentityKey(displayName, channelSlug);
            var appearanceCount = existingCandidates.Count(item =>
                streamerIdentityId.HasValue && item.StreamerIdentityId == streamerIdentityId
                || string.Equals(WorkflowRuleSettings.CandidateIdentityKey(item.DisplayName, item.ChannelSlug), identityKey, StringComparison.Ordinal));
            if (appearanceCount >= appearancesRule.Limit)
            {
                return CreateModerationWorkflowRuleError(
                    $"Diese Person ist bereits {appearanceCount}x als Kandidat:in eingetragen. Limit: {appearancesRule.Limit}.");
            }
        }

        return null;
    }

    private static IResult CreateModerationWorkflowRuleError(string message) =>
        Results.BadRequest(new { message = $"Workflow-Regel blockiert: {message}" });

    private static void ApplyTrackingReviewDecision(Nomination nomination, string? reviewNote, string reviewerTwitchUserId)
    {
        var trackingFlags = TrackingRulesSettings.ReadFlagHits(nomination.TrackingFlagsJson);
        if (trackingFlags.Length == 0)
        {
            nomination.TrackingReviewStatus = "clear";
            nomination.TrackingReviewNote = null;
            nomination.TrackingReviewedByTwitchId = null;
            nomination.TrackingReviewedAt = null;
            return;
        }

        var note = string.IsNullOrWhiteSpace(reviewNote) ? null : reviewNote.Trim();
        nomination.TrackingReviewStatus = trackingFlags.Any(flag => flag.AdminNoteRequiredOnOverride && !string.IsNullOrWhiteSpace(note))
            ? "overridden"
            : "reviewed";
        nomination.TrackingReviewNote = note;
        nomination.TrackingReviewedByTwitchId = reviewerTwitchUserId;
        nomination.TrackingReviewedAt = DateTimeOffset.UtcNow;
    }
}
