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

namespace Backend.Endpoints;

public static partial class PublicEndpoints
{
    private static async Task<IResult> CreateVote(
        HttpContext context,
        CreateVoteRequest request,
        AwardsDbContext db,
        IUserSessionService userSessionService,
        IRiskFlagService riskFlagService,
        IRiskRuleService riskRuleService)
    {
        if (request.Entries.Length == 0)
        {
            return Results.BadRequest(new { message = "At least one vote entry is required." });
        }

        var distinctCategoryCount = request.Entries
            .Select(item => item.CategoryId)
            .Distinct()
            .Count();

        if (distinctCategoryCount != request.Entries.Length)
        {
            return Results.BadRequest(new { message = "Only one vote entry per category is allowed." });
        }

        var season = await db.Seasons
            .AsNoTracking()
            .FirstOrDefaultAsync(item => item.Id == request.SeasonId);
        var voteSeasonResolution = EnsurePublicWriteSeason(season, "voting");
        if (voteSeasonResolution.Result is not null)
        {
            return voteSeasonResolution.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 candidateIds = request.Entries.Select(item => item.CandidateId).Distinct().ToArray();
        var validCandidates = await db.Candidates
            .AsNoTracking()
            .Where(item => item.SeasonId == request.SeasonId && candidateIds.Contains(item.Id))
            .Select(item => new { item.Id, item.CategoryId })
            .ToArrayAsync();

        if (validCandidates.Length != candidateIds.Length)
        {
            return Results.BadRequest(new { message = "One or more selected candidates do not belong to this season." });
        }

        var candidateCategoryMap = validCandidates.ToDictionary(item => item.Id, item => item.CategoryId);
        if (request.Entries.Any(item => candidateCategoryMap[item.CandidateId] != item.CategoryId))
        {
            return Results.BadRequest(new { message = "A selected candidate does not match the submitted category." });
        }

        var ballot = await db.VoteBallots
            .Include(item => item.Entries)
            .FirstOrDefaultAsync(item => item.SeasonId == request.SeasonId && item.SubmittedByTwitchId == submitterId);

        var isResubmission = ballot is not null;
        ballot = await ApplyVoteEntriesAsync(db, ballot, request.SeasonId, submitterId, request.Entries, context.RequestAborted);

        var resubmittedBallotRule = await riskRuleService.GetRuleAsync("resubmitted_ballot", context.RequestAborted);
        var rapidVoteUpdatesRule = await riskRuleService.GetRuleAsync("rapid_vote_updates", context.RequestAborted);
        var recentVoteSubmissions = await db.VoteBallots.CountAsync(item =>
            item.SubmittedByTwitchId == submitterId
            && item.SubmittedAt >= DateTimeOffset.UtcNow.AddMinutes(-rapidVoteUpdatesRule.WindowMinutes));

        try
        {
            await db.SaveChangesAsync(context.RequestAborted);
        }
        catch (DbUpdateException error) when (!isResubmission && IsUniqueVoteBallotViolation(error))
        {
            db.ChangeTracker.Clear();
            ballot = await db.VoteBallots
                .Include(item => item.Entries)
                .FirstAsync(item => item.SeasonId == request.SeasonId && item.SubmittedByTwitchId == submitterId, context.RequestAborted);
            isResubmission = true;
            ballot = await ApplyVoteEntriesAsync(db, ballot, request.SeasonId, submitterId, request.Entries, context.RequestAborted);
            await db.SaveChangesAsync(context.RequestAborted);
        }

        var ballotLink = new
        {
            label = "Voting-Analytics öffnen",
            entityType = "vote",
            entityId = ballot.Id.ToString(),
            to = $"/admin/analytics?query={Uri.EscapeDataString(submitterId)}",
        };

        if (isResubmission && resubmittedBallotRule.Enabled)
        {
            await riskFlagService.AddIfMissingAsync(
                request.SeasonId,
                submitterId,
                "vote",
                "resubmitted_ballot",
                resubmittedBallotRule.Severity,
                "Ein User hat sein Ballot erneut gespeichert oder aktualisiert.",
                requestMetadata,
                new { ballotId = ballot.Id, entryCount = request.Entries.Length, entityLinks = new[] { ballotLink } },
                context.RequestAborted);
        }

        if (rapidVoteUpdatesRule.Enabled && recentVoteSubmissions >= rapidVoteUpdatesRule.Threshold)
        {
            await riskFlagService.AddIfMissingAsync(
                request.SeasonId,
                submitterId,
                "vote",
                "rapid_vote_updates",
                rapidVoteUpdatesRule.Severity,
                "Mehrere Voting-Aenderungen wurden in kurzer Zeit erkannt.",
                requestMetadata,
                new { ballotId = ballot.Id, recentVoteSubmissions, threshold = rapidVoteUpdatesRule.Threshold, windowMinutes = rapidVoteUpdatesRule.WindowMinutes, entityLinks = new[] { ballotLink } },
                context.RequestAborted);
        }

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { ballotId = ballot.Id, entries = ballot.Entries.Count, updated = isResubmission });
    }

    private static async Task<VoteBallot> ApplyVoteEntriesAsync(
        AwardsDbContext db,
        VoteBallot? ballot,
        int seasonId,
        string submitterId,
        VoteEntryRequest[] entries,
        CancellationToken cancellationToken)
    {
        if (ballot is null)
        {
            ballot = new VoteBallot
            {
                SeasonId = seasonId,
                SubmittedByTwitchId = submitterId,
            };

            await db.VoteBallots.AddAsync(ballot, cancellationToken);
        }
        else
        {
            db.VoteEntries.RemoveRange(ballot.Entries);
            ballot.Entries.Clear();
        }

        ballot.SubmittedAt = DateTimeOffset.UtcNow;
        ballot.Status = "submitted";
        ballot.Entries = entries.Select(entry => new VoteEntry
        {
            CategoryId = entry.CategoryId,
            CandidateId = entry.CandidateId,
        }).ToList();

        return ballot;
    }

    private static bool IsUniqueVoteBallotViolation(DbUpdateException error) =>
        error.InnerException is PostgresException { SqlState: PostgresErrorCodes.UniqueViolation };
}
