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 readonly string[] CandidateAcceptanceStatuses = ["open", "contacted", "accepted", "declined"];
    private static readonly string[] CandidateClipEmbedStatuses = ["unchecked", "embeddable", "link_only", "blocked"];

    private static async Task<IResult> CreateCandidate(
        HttpContext context,
        int seasonId,
        UpsertCandidateRequest request,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);
        var validationError = ValidateCandidateRequest(request);
        if (validationError is not null)
        {
            return validationError;
        }

        var category = await db.Categories.FirstOrDefaultAsync(item => item.Id == request.CategoryId && item.SeasonId == seasonId);
        if (category is null)
        {
            return Results.BadRequest(new { message = "The selected category does not exist in this season." });
        }

        var normalizedDisplayName = request.DisplayName.Trim();
        var normalizedChannelSlug = request.ChannelSlug.Trim();
        var normalizedAcceptanceStatus = NormalizeCandidateChoice(request.AcceptanceStatus, "open", CandidateAcceptanceStatuses);
        var normalizedAcceptanceNote = NormalizeOptionalCandidateText(request.AcceptanceNote);
        var normalizedClipUrl = NormalizeOptionalCandidateUrl(request.ClipCompilationUrl);
        var normalizedClipTitle = NormalizeOptionalCandidateText(request.ClipCompilationTitle);
        var normalizedClipPlatform = NormalizeOptionalCandidateText(request.ClipCompilationPlatform);
        var normalizedClipEmbedStatus = NormalizeCandidateChoice(request.ClipEmbedStatus, "unchecked", CandidateClipEmbedStatuses);

        if (normalizedClipUrl is null)
        {
            normalizedClipTitle = null;
            normalizedClipPlatform = null;
            normalizedClipEmbedStatus = "unchecked";
        }

        if (await db.Candidates.AnyAsync(item =>
                item.SeasonId == seasonId
                && item.CategoryId == request.CategoryId
                && (item.DisplayName.ToLower() == normalizedDisplayName.ToLower()
                    || item.ChannelSlug.ToLower() == normalizedChannelSlug.ToLower())))
        {
            return Results.BadRequest(new { message = "A candidate with the same display name or channel slug already exists in this category." });
        }

        var workflowRuleBlock = await BuildCandidateWorkflowRuleBlockAsync(
            db,
            seasonId,
            request.CategoryId,
            null,
            null,
            normalizedDisplayName,
            normalizedChannelSlug,
            normalizedAcceptanceStatus,
            context.RequestAborted);
        if (workflowRuleBlock is not null)
        {
            return workflowRuleBlock;
        }

        var candidate = new Candidate
        {
            SeasonId = seasonId,
            CategoryId = request.CategoryId,
            DisplayName = normalizedDisplayName,
            ChannelSlug = normalizedChannelSlug,
            Platform = request.Platform.Trim(),
            AcceptanceStatus = normalizedAcceptanceStatus,
            AcceptanceNote = normalizedAcceptanceNote,
            ClipCompilationUrl = normalizedClipUrl,
            ClipCompilationTitle = normalizedClipTitle,
            ClipCompilationPlatform = normalizedClipPlatform,
            ClipEmbedStatus = normalizedClipEmbedStatus,
        };

        db.Candidates.Add(candidate);
        adminAuditService.AddEntry(
            session.TwitchUserId,
            "candidate.create",
            "candidate",
            request.DisplayName.Trim(),
            $"Kandidat {request.DisplayName.Trim()} wurde angelegt.",
            new { seasonId, request.CategoryId, request.Platform, acceptanceStatus = normalizedAcceptanceStatus, hasCompilation = normalizedClipUrl is not null },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { saved = true, candidateId = candidate.Id });
    }

    private static async Task<IResult> UpdateCandidate(
        HttpContext context,
        int candidateId,
        UpsertCandidateRequest request,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);
        var validationError = ValidateCandidateRequest(request);
        if (validationError is not null)
        {
            return validationError;
        }

        var candidate = await db.Candidates.FirstOrDefaultAsync(item => item.Id == candidateId);
        if (candidate is null)
        {
            return Results.NotFound();
        }

        var targetCategory = await db.Categories.FirstOrDefaultAsync(item =>
            item.Id == request.CategoryId && item.SeasonId == candidate.SeasonId);
        if (targetCategory is null)
        {
            return Results.BadRequest(new { message = "The selected category does not exist in this season." });
        }

        var normalizedDisplayName = request.DisplayName.Trim();
        var normalizedChannelSlug = request.ChannelSlug.Trim();
        var normalizedAcceptanceStatus = NormalizeCandidateChoice(request.AcceptanceStatus, "open", CandidateAcceptanceStatuses);
        var normalizedAcceptanceNote = NormalizeOptionalCandidateText(request.AcceptanceNote);
        var normalizedClipUrl = NormalizeOptionalCandidateUrl(request.ClipCompilationUrl);
        var normalizedClipTitle = NormalizeOptionalCandidateText(request.ClipCompilationTitle);
        var normalizedClipPlatform = NormalizeOptionalCandidateText(request.ClipCompilationPlatform);
        var normalizedClipEmbedStatus = NormalizeCandidateChoice(request.ClipEmbedStatus, "unchecked", CandidateClipEmbedStatuses);

        if (normalizedClipUrl is null)
        {
            normalizedClipTitle = null;
            normalizedClipPlatform = null;
            normalizedClipEmbedStatus = "unchecked";
        }

        if (await db.Candidates.AnyAsync(item =>
                item.SeasonId == candidate.SeasonId
                && item.CategoryId == request.CategoryId
                && item.Id != candidateId
                && (item.DisplayName.ToLower() == normalizedDisplayName.ToLower()
                    || item.ChannelSlug.ToLower() == normalizedChannelSlug.ToLower())))
        {
            return Results.BadRequest(new { message = "A candidate with the same display name or channel slug already exists in this category." });
        }

        var workflowRuleBlock = await BuildCandidateWorkflowRuleBlockAsync(
            db,
            candidate.SeasonId,
            request.CategoryId,
            candidateId,
            candidate.StreamerIdentityId,
            normalizedDisplayName,
            normalizedChannelSlug,
            normalizedAcceptanceStatus,
            context.RequestAborted);
        if (workflowRuleBlock is not null)
        {
            return workflowRuleBlock;
        }

        candidate.CategoryId = request.CategoryId;
        candidate.DisplayName = normalizedDisplayName;
        candidate.ChannelSlug = normalizedChannelSlug;
        candidate.Platform = request.Platform.Trim();
        candidate.AcceptanceStatus = normalizedAcceptanceStatus;
        candidate.AcceptanceNote = normalizedAcceptanceNote;
        candidate.ClipCompilationUrl = normalizedClipUrl;
        candidate.ClipCompilationTitle = normalizedClipTitle;
        candidate.ClipCompilationPlatform = normalizedClipPlatform;
        candidate.ClipEmbedStatus = normalizedClipEmbedStatus;

        adminAuditService.AddEntry(
            session.TwitchUserId,
            "candidate.update",
            "candidate",
            candidate.Id.ToString(),
            $"Kandidat {request.DisplayName.Trim()} wurde aktualisiert.",
            new { request.CategoryId, request.Platform, acceptanceStatus = normalizedAcceptanceStatus, hasCompilation = normalizedClipUrl is not null },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { saved = true, candidateId = candidate.Id });
    }

    private static async Task<IResult> GetCandidateDeletePreview(
        int candidateId,
        AwardsDbContext db,
        CancellationToken cancellationToken)
    {
        var candidate = await db.Candidates
            .AsNoTracking()
            .FirstOrDefaultAsync(item => item.Id == candidateId, cancellationToken);
        if (candidate is null)
        {
            return Results.NotFound();
        }

        var nominationCount = await db.Nominations.CountAsync(item => item.CandidateId == candidateId, cancellationToken);
        var clipCount = await db.ClipSubmissions.CountAsync(item => item.CandidateId == candidateId, cancellationToken);
        var voteCount = await db.VoteEntries.CountAsync(item => item.CandidateId == candidateId, cancellationToken);
        var resultCount = await db.Results.CountAsync(item => item.CandidateId == candidateId, cancellationToken);

        return Results.Ok(new
        {
            candidateId,
            nominationCount,
            clipCount,
            voteCount,
            resultCount,
        });
    }

    private static async Task<IResult> DeleteCandidate(
        HttpContext context,
        int candidateId,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);
        var candidate = await db.Candidates.FirstOrDefaultAsync(item => item.Id == candidateId);
        if (candidate is null)
        {
            return Results.NotFound();
        }

        var linkedNominations = await db.Nominations
            .Where(item => item.CandidateId == candidateId)
            .ToListAsync(context.RequestAborted);
        if (linkedNominations.Count > 0)
        {
            db.Nominations.RemoveRange(linkedNominations);
        }

        var linkedClips = await db.ClipSubmissions
            .Where(item => item.CandidateId == candidateId)
            .ToListAsync(context.RequestAborted);
        if (linkedClips.Count > 0)
        {
            db.ClipSubmissions.RemoveRange(linkedClips);
        }

        var linkedVoteEntries = await db.VoteEntries
            .Where(item => item.CandidateId == candidateId)
            .ToListAsync(context.RequestAborted);
        if (linkedVoteEntries.Count > 0)
        {
            db.VoteEntries.RemoveRange(linkedVoteEntries);
        }

        var linkedResults = await db.Results
            .Where(item => item.CandidateId == candidateId)
            .ToListAsync(context.RequestAborted);
        if (linkedResults.Count > 0)
        {
            db.Results.RemoveRange(linkedResults);
        }

        db.Candidates.Remove(candidate);
        adminAuditService.AddEntry(
            session.TwitchUserId,
            "candidate.delete",
            "candidate",
            candidate.Id.ToString(),
            $"Kandidat {candidate.DisplayName} wurde gelöscht.",
            new
            {
                candidate.CategoryId,
                candidate.Platform,
                deletedNominations = linkedNominations.Count,
                deletedClips = linkedClips.Count,
                deletedVoteEntries = linkedVoteEntries.Count,
                deletedResults = linkedResults.Count,
            },
            RequestMetadataReader.Read(context));

        try
        {
            await db.SaveChangesAsync(context.RequestAborted);
            return Results.Ok(new { deleted = true, candidateId });
        }
        catch (DbUpdateException)
        {
            return Results.BadRequest(new
            {
                message = "Kandidat konnte nicht gelöscht werden, weil noch verknüpfte Daten blockieren.",
            });
        }
    }

    private static string NormalizeCandidateChoice(string? value, string fallback, IReadOnlyCollection<string> allowedValues)
    {
        var normalized = value?.Trim().ToLowerInvariant();
        return !string.IsNullOrWhiteSpace(normalized) && allowedValues.Contains(normalized)
            ? normalized
            : fallback;
    }

    private static string? NormalizeOptionalCandidateText(string? value)
    {
        var normalized = value?.Trim();
        return string.IsNullOrWhiteSpace(normalized) ? null : normalized;
    }

    private static string? NormalizeOptionalCandidateUrl(string? value)
    {
        var normalized = value?.Trim();
        if (string.IsNullOrWhiteSpace(normalized))
        {
            return null;
        }

        if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri) || uri.Scheme is not ("http" or "https"))
        {
            throw new BadHttpRequestException("Compilation-Link muss eine gültige http(s)-URL sein.");
        }

        return uri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped);
    }
}
