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 AdminSeasonManagementEndpoints
{
    private const int MaxCategoryGroupNameLength = 80;
    private const int MaxCategoryNameLength = 120;
    private const int MaxCategorySlugLength = 120;
    private const int MaxCategoryDescriptionLength = 600;
    private const int MaxCandidateDisplayNameLength = 120;
    private const int MaxCandidateChannelSlugLength = 120;
    private const int MaxCandidatePlatformLength = 60;
    private const int MaxCandidateAcceptanceNoteLength = 500;
    private const int MaxCandidateClipUrlLength = 500;
    private const int MaxCandidateClipTitleLength = 200;
    private const int MaxCandidateClipPlatformLength = 40;

    private sealed record CandidateRuleSnapshot(
        int Id,
        int CategoryId,
        int? StreamerIdentityId,
        string DisplayName,
        string ChannelSlug,
        string AcceptanceStatus);

    private sealed record CandidateReadinessSnapshot(
        int CategoryId,
        int? StreamerIdentityId,
        string DisplayName,
        string ChannelSlug,
        string AcceptanceStatus,
        string? ClipCompilationUrl);

    private sealed record WinnerReadinessSnapshot(
        int CategoryId,
        int? StreamerIdentityId,
        string DisplayName,
        string ChannelSlug,
        string? ClipCompilationUrl);

    private sealed record WinnerPublicationSnapshot(
        int CategoryId,
        int? StreamerIdentityId,
        string DisplayName,
        string ChannelSlug,
        string? ClipCompilationUrl);

    private static IResult? ValidateSeasonRequest(CreateSeasonRequest request)
    {
        if (request.Year < 2020 || request.Year > 2100)
        {
            return Results.BadRequest(new { message = "Please provide a valid award year." });
        }

        if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.CurrentPhase))
        {
            return Results.BadRequest(new { message = "Season name and current phase are required." });
        }

        if (!IsKnownSeasonPhase(request.CurrentPhase))
        {
            return Results.BadRequest(new { message = "Current phase must be nomination, voting, preparation, show, or completed." });
        }

        if (!SeasonMappings.IsSeasonScheduleValid(
                request.NominationStartsAt,
                request.NominationEndsAt,
                request.VotingStartsAt,
                request.VotingEndsAt,
                request.ReviewStartsAt,
                request.ReviewEndsAt,
                request.ShowDate))
        {
            return Results.BadRequest(new { message = "The season schedule is not in chronological order." });
        }

        return null;
    }

    private static IResult? ValidateSeasonRequest(UpdateSeasonRequest request)
    {
        if (request.Year < 2020 || request.Year > 2100)
        {
            return Results.BadRequest(new { message = "Please provide a valid award year." });
        }

        if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.CurrentPhase))
        {
            return Results.BadRequest(new { message = "Season name and current phase are required." });
        }

        if (!IsKnownSeasonPhase(request.CurrentPhase))
        {
            return Results.BadRequest(new { message = "Current phase must be nomination, voting, preparation, show, or completed." });
        }

        if (!SeasonMappings.IsSeasonScheduleValid(
                request.NominationStartsAt,
                request.NominationEndsAt,
                request.VotingStartsAt,
                request.VotingEndsAt,
                request.ReviewStartsAt,
                request.ReviewEndsAt,
                request.ShowDate))
        {
            return Results.BadRequest(new { message = "The season schedule is not in chronological order." });
        }

        return null;
    }

    private static bool IsKnownSeasonPhase(string? currentPhase)
    {
        var value = currentPhase?.Trim().ToLowerInvariant() ?? string.Empty;
        return value.Contains("show")
            || value.Contains("abgeschlossen")
            || value.Contains("archiv")
            || value.Contains("complete")
            || value.Contains("ended")
            || value.Contains("aufbereit")
            || value.Contains("vorbereit")
            || value.Contains("pause")
            || value.Contains("review")
            || value.Contains("auswert")
            || value.Contains("vot")
            || value.Contains("nomin");
    }

    private static async Task UnsetOtherCurrentSeasonsAsync(
        AwardsDbContext db,
        bool shouldUnsetOthers,
        int? seasonIdToKeep,
        CancellationToken cancellationToken)
    {
        if (!shouldUnsetOthers)
        {
            return;
        }

        var activeSeasons = await db.Seasons
            .Where(item => item.IsCurrent && (!seasonIdToKeep.HasValue || item.Id != seasonIdToKeep.Value))
            .ToListAsync(cancellationToken);

        foreach (var activeSeason in activeSeasons)
        {
            activeSeason.IsCurrent = false;
        }
    }

    private static IResult CreateReadinessError(IEnumerable<string> issues)
    {
        var issueList = issues.ToArray();
        return Results.BadRequest(new
        {
            message = $"Landingpage-Freigabe blockiert: {string.Join(" ", issueList)}",
            issues = issueList,
        });
    }

    private static IResult CreateWinnerPublicationError(IEnumerable<string> issues)
    {
        var issueList = issues.ToArray();
        return Results.BadRequest(new
        {
            message = $"Gewinner-Freigabe blockiert: {string.Join(" ", issueList)}",
            issues = issueList,
        });
    }

    private static async Task<WorkflowRuleSetting[]> LoadWorkflowRulesAsync(AwardsDbContext db, int seasonId, CancellationToken cancellationToken)
    {
        var season = await db.Seasons
            .AsNoTracking()
            .FirstOrDefaultAsync(item => item.Id == seasonId, cancellationToken);
        var settings = await db.SiteSettings
            .AsNoTracking()
            .FirstOrDefaultAsync(item => item.Id == 1, cancellationToken);

        return WorkflowRuleSettings.Read(season, settings);
    }

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

    private static async Task<IResult?> BuildCandidateWorkflowRuleBlockAsync(
        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 rules = await LoadWorkflowRulesAsync(db, seasonId, cancellationToken);
        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 CandidateRuleSnapshot(
                item.Id,
                item.CategoryId,
                item.StreamerIdentityId,
                item.DisplayName,
                item.ChannelSlug,
                item.AcceptanceStatus))
            .ToArrayAsync(cancellationToken);

        if (WorkflowRuleSettings.ShouldBlock(finalistsRule))
        {
            var categoryCount = existingCandidates.Count(item => item.CategoryId == categoryId);
            if (categoryCount >= finalistsRule.Limit)
            {
                return CreateWorkflowRuleError(
                    $"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 CreateWorkflowRuleError(
                    $"Diese Person ist bereits {appearanceCount}x als Kandidat:in eingetragen. Limit: {appearancesRule.Limit}.");
            }
        }

        return null;
    }

    private static string[] BuildNewSeasonReadinessIssues(
        string currentPhase,
        bool isCurrent,
        int copiedCategoryCount)
    {
        var phaseKey = SeasonMappings.NormalizePhaseKey(currentPhase);
        var needsCandidateReadiness = RequiresCandidateReadiness(phaseKey, isCurrent);
        var needsWinnerReadiness = RequiresWinnerReadiness(phaseKey);
        if (!isCurrent && !needsWinnerReadiness)
        {
            return [];
        }

        var issues = new List<string>();
        if (copiedCategoryCount <= 0)
        {
            issues.Add("Mindestens eine Kategorie ist erforderlich.");
        }

        if (needsCandidateReadiness)
        {
            issues.Add("Kandidaten muessen vor dieser Phase fuer alle Kategorien gepflegt sein.");
        }

        if (needsWinnerReadiness)
        {
            issues.Add("Abgeschlossen ist erst moeglich, wenn jede Kategorie einen Gewinner hat.");
        }

        return issues.ToArray();
    }

    private static async Task<string[]> BuildSeasonReadinessIssuesAsync(
        AwardsDbContext db,
        int seasonId,
        string currentPhase,
        bool isCurrent,
        CancellationToken cancellationToken)
    {
        var season = await db.Seasons
            .AsNoTracking()
            .FirstOrDefaultAsync(item => item.Id == seasonId, cancellationToken);
        if (season is null)
        {
            return ["Das Award-Jahr konnte fuer die Readiness-Pruefung nicht gefunden werden."];
        }

        var phaseKey = SeasonMappings.NormalizePhaseKey(currentPhase);
        var needsCandidateReadiness = RequiresCandidateReadiness(phaseKey, isCurrent);
        var needsWinnerReadiness = RequiresWinnerReadiness(phaseKey);
        if (!isCurrent && !needsWinnerReadiness)
        {
            return [];
        }

        var workflowRules = await LoadWorkflowRulesAsync(db, seasonId, cancellationToken);
        var appearancesRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.MaxCandidateAppearances);
        var winnerPlacementsRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.MaxWinnerPlacements);
        var winnerRequiresClipRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.WinnerRequiresClip);

        var categoryIds = await db.Categories
            .AsNoTracking()
            .Where(item => item.SeasonId == seasonId)
            .Select(item => item.Id)
            .ToArrayAsync(cancellationToken);

        var issues = new List<string>();
        if (categoryIds.Length == 0)
        {
            issues.Add("Mindestens eine Kategorie ist erforderlich.");
        }

        if (needsCandidateReadiness && categoryIds.Length > 0)
        {
            var candidateSnapshots = await db.Candidates
                .AsNoTracking()
                .Where(item => item.SeasonId == seasonId)
                .Select(item => new CandidateReadinessSnapshot(
                    item.CategoryId,
                    item.StreamerIdentityId,
                    item.DisplayName,
                    item.ChannelSlug,
                    item.AcceptanceStatus,
                    item.ClipCompilationUrl))
                .ToArrayAsync(cancellationToken);
            var activeCandidates = candidateSnapshots
                .Where(item => !string.Equals(item.AcceptanceStatus, "declined", StringComparison.OrdinalIgnoreCase))
                .ToArray();
            var categoriesWithCandidates = activeCandidates
                .Select(item => item.CategoryId)
                .Distinct()
                .Count();
            var emptyCategories = Math.Max(0, categoryIds.Length - categoriesWithCandidates);
            if (emptyCategories > 0)
            {
                issues.Add($"{emptyCategories} Kategorien haben noch keine voting-bereiten Kandidaten.");
            }

            if (WorkflowRuleSettings.ShouldBlock(appearancesRule))
            {
                var identityOverflow = activeCandidates
                    .GroupBy(item => ResolveCandidateIdentityKey(item.StreamerIdentityId, item.DisplayName, item.ChannelSlug))
                    .Select(group => new
                    {
                        Count = group.Count(),
                        DisplayName = group.Select(item => item.DisplayName).FirstOrDefault(name => !string.IsNullOrWhiteSpace(name)) ?? "Unbekannt",
                    })
                    .Where(item => item.Count > appearancesRule.Limit)
                    .OrderByDescending(item => item.Count)
                    .FirstOrDefault();
                if (identityOverflow is not null)
                {
                    issues.Add(
                        $"Workflow-Regel blockiert: {identityOverflow.DisplayName} ist bereits {identityOverflow.Count}x als Kandidat:in eingetragen. Limit: {appearancesRule.Limit}.");
                }
            }
        }

        if (needsWinnerReadiness && categoryIds.Length > 0)
        {
            if (season.ShowDate > DateOnly.FromDateTime(DateTime.Now))
            {
                issues.Add("Die Award Show liegt noch nicht in der Vergangenheit.");
            }

            var categoriesWithResults = await db.Results
                .AsNoTracking()
                .Where(item => item.SeasonId == seasonId)
                .Select(item => item.CategoryId)
                .Distinct()
                .CountAsync(cancellationToken);
            var missingResults = Math.Max(0, categoryIds.Length - categoriesWithResults);
            if (missingResults > 0)
            {
                issues.Add($"{missingResults} Kategorien haben noch keinen Gewinner.");
            }

            var resultSnapshots = await db.Results
                .AsNoTracking()
                .Where(item => item.SeasonId == seasonId)
                .Select(item => new WinnerReadinessSnapshot(
                    item.CategoryId,
                    item.Candidate.StreamerIdentityId,
                    item.Candidate.DisplayName,
                    item.Candidate.ChannelSlug,
                    item.Candidate.ClipCompilationUrl))
                .ToArrayAsync(cancellationToken);

            if (WorkflowRuleSettings.ShouldBlock(winnerRequiresClipRule))
            {
                var missingWinnerClipCount = resultSnapshots.Count(item => string.IsNullOrWhiteSpace(item.ClipCompilationUrl));
                if (missingWinnerClipCount > 0)
                {
                    issues.Add($"{missingWinnerClipCount} Gewinner haben noch keinen gepflegten Clip-Link.");
                }
            }

            if (WorkflowRuleSettings.ShouldBlock(winnerPlacementsRule))
            {
                var winnerOverflow = resultSnapshots
                    .GroupBy(item => ResolveCandidateIdentityKey(item.StreamerIdentityId, item.DisplayName, item.ChannelSlug))
                    .Select(group => new
                    {
                        Count = group.Count(),
                        DisplayName = group.Select(item => item.DisplayName).FirstOrDefault(name => !string.IsNullOrWhiteSpace(name)) ?? "Unbekannt",
                    })
                    .Where(item => item.Count > winnerPlacementsRule.Limit)
                    .OrderByDescending(item => item.Count)
                    .FirstOrDefault();
                if (winnerOverflow is not null)
                {
                    issues.Add(
                        $"Workflow-Regel blockiert: {winnerOverflow.DisplayName} hat bereits {winnerOverflow.Count} Gewinnerplatz(e). Limit: {winnerPlacementsRule.Limit}.");
                }
            }
        }

        return issues.ToArray();
    }

    private static async Task<string[]> BuildWinnerPublicationIssuesAsync(
        AwardsDbContext db,
        int seasonId,
        CancellationToken cancellationToken)
    {
        var categoryIds = await db.Categories
            .AsNoTracking()
            .Where(item => item.SeasonId == seasonId)
            .Select(item => item.Id)
            .ToArrayAsync(cancellationToken);

        var issues = new List<string>();
        if (categoryIds.Length == 0)
        {
            issues.Add("Mindestens eine Kategorie ist erforderlich.");
            return issues.ToArray();
        }

        var resultSnapshots = await db.Results
            .AsNoTracking()
            .Where(item => item.SeasonId == seasonId)
            .Select(item => new WinnerPublicationSnapshot(
                item.CategoryId,
                item.Candidate.StreamerIdentityId,
                item.Candidate.DisplayName,
                item.Candidate.ChannelSlug,
                item.Candidate.ClipCompilationUrl))
            .ToArrayAsync(cancellationToken);

        var categoriesWithResults = resultSnapshots
            .Select(item => item.CategoryId)
            .Distinct()
            .Count();
        var missingResults = Math.Max(0, categoryIds.Length - categoriesWithResults);
        if (missingResults > 0)
        {
            issues.Add($"{missingResults} Kategorien haben noch keinen Gewinner.");
        }

        var openReviewCount = await db.Nominations
            .AsNoTracking()
            .CountAsync(item => item.SeasonId == seasonId && item.Status == "pending", cancellationToken);
        if (openReviewCount > 0)
        {
            issues.Add($"{openReviewCount} Nominierungs-Review(s) sind noch offen.");
        }

        var workflowRules = await LoadWorkflowRulesAsync(db, seasonId, cancellationToken);
        var winnerRequiresClipRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.WinnerRequiresClip);
        if (WorkflowRuleSettings.ShouldBlock(winnerRequiresClipRule))
        {
            var missingWinnerClipCount = resultSnapshots.Count(item => string.IsNullOrWhiteSpace(item.ClipCompilationUrl));
            if (missingWinnerClipCount > 0)
            {
                issues.Add($"{missingWinnerClipCount} Gewinner haben noch keinen gepflegten Clip-Link.");
            }
        }

        var winnerPlacementsRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.MaxWinnerPlacements);
        if (WorkflowRuleSettings.ShouldBlock(winnerPlacementsRule))
        {
            var winnerOverflow = resultSnapshots
                .GroupBy(item => ResolveCandidateIdentityKey(item.StreamerIdentityId, item.DisplayName, item.ChannelSlug))
                .Select(group => new
                {
                    Count = group.Count(),
                    DisplayName = group.Select(item => item.DisplayName).FirstOrDefault(name => !string.IsNullOrWhiteSpace(name)) ?? "Unbekannt",
                })
                .Where(item => item.Count > winnerPlacementsRule.Limit)
                .OrderByDescending(item => item.Count)
                .FirstOrDefault();
            if (winnerOverflow is not null)
            {
                issues.Add(
                    $"Workflow-Regel blockiert: {winnerOverflow.DisplayName} hat bereits {winnerOverflow.Count} Gewinnerplatz(e). Limit: {winnerPlacementsRule.Limit}.");
            }
        }

        return issues.ToArray();
    }

    private static string ResolveCandidateIdentityKey(int? streamerIdentityId, string displayName, string channelSlug)
    {
        if (streamerIdentityId.HasValue)
        {
            return $"identity:{streamerIdentityId.Value}";
        }

        return WorkflowRuleSettings.CandidateIdentityKey(displayName, channelSlug);
    }

    private static bool RequiresCandidateReadiness(string phaseKey, bool isCurrent)
    {
        return (isCurrent && !string.Equals(phaseKey, "nomination", StringComparison.Ordinal))
            || string.Equals(phaseKey, "completed", StringComparison.Ordinal);
    }

    private static bool RequiresWinnerReadiness(string phaseKey)
    {
        return string.Equals(phaseKey, "completed", StringComparison.Ordinal);
    }

    private static IResult? ValidateCategoryRequest(UpsertCategoryRequest request)
    {
        var groupName = request.GroupName.Trim();
        var name = request.Name.Trim();
        var slug = request.Slug.Trim();
        var description = request.Description.Trim();

        if (string.IsNullOrWhiteSpace(groupName) || groupName.Length > MaxCategoryGroupNameLength)
        {
            return Results.BadRequest(new { message = $"Category group name is required and must stay below {MaxCategoryGroupNameLength} characters." });
        }

        if (string.IsNullOrWhiteSpace(name) || name.Length > MaxCategoryNameLength)
        {
            return Results.BadRequest(new { message = $"Category name is required and must stay below {MaxCategoryNameLength} characters." });
        }

        if (string.IsNullOrWhiteSpace(slug) || slug.Length > MaxCategorySlugLength)
        {
            return Results.BadRequest(new { message = $"Category slug is required and must stay below {MaxCategorySlugLength} characters." });
        }

        if (!slug.All(value => char.IsLetterOrDigit(value) || value is '-' or '_'))
        {
            return Results.BadRequest(new { message = "Category slug contains unsupported characters." });
        }

        if (description.Length > MaxCategoryDescriptionLength)
        {
            return Results.BadRequest(new { message = $"Category description must stay below {MaxCategoryDescriptionLength} characters." });
        }

        if (request.SortOrder is < 0 or > 500)
        {
            return Results.BadRequest(new { message = "Category sort order must be between 0 and 500." });
        }

        if (request.MaxNomineesPerUser is < 1 or > 10)
        {
            return Results.BadRequest(new { message = "Max nominees per user must be between 1 and 10." });
        }

        if (request.ViewerRangeMin is < 0 or > 100000)
        {
            return Results.BadRequest(new { message = "Viewer range start must be between 0 and 100000." });
        }

        if (request.ViewerRangeMax is < 0 or > 100000)
        {
            return Results.BadRequest(new { message = "Viewer range end must be between 0 and 100000." });
        }

        if (request.ViewerRangeMin is not null && request.ViewerRangeMax is not null && request.ViewerRangeMax < request.ViewerRangeMin)
        {
            return Results.BadRequest(new { message = "Viewer range end must be greater than or equal to the start." });
        }

        return null;
    }

    private static IResult? ValidateCandidateRequest(UpsertCandidateRequest request)
    {
        var displayName = request.DisplayName.Trim();
        var channelSlug = request.ChannelSlug.Trim();
        var platform = request.Platform.Trim();

        if (request.CategoryId <= 0)
        {
            return Results.BadRequest(new { message = "A valid category is required." });
        }

        if (string.IsNullOrWhiteSpace(displayName) || displayName.Length > MaxCandidateDisplayNameLength)
        {
            return Results.BadRequest(new { message = $"Display name is required and must stay below {MaxCandidateDisplayNameLength} characters." });
        }

        if (string.IsNullOrWhiteSpace(channelSlug) || channelSlug.Length > MaxCandidateChannelSlugLength)
        {
            return Results.BadRequest(new { message = $"Channel slug is required and must stay below {MaxCandidateChannelSlugLength} characters." });
        }

        if (!channelSlug.All(value => char.IsLetterOrDigit(value) || value is '-' or '_' or '.'))
        {
            return Results.BadRequest(new { message = "Channel slug contains unsupported characters." });
        }

        if (string.IsNullOrWhiteSpace(platform) || platform.Length > MaxCandidatePlatformLength)
        {
            return Results.BadRequest(new { message = $"Platform is required and must stay below {MaxCandidatePlatformLength} characters." });
        }

        if (!IsAllowedCandidateChoice(request.AcceptanceStatus, CandidateAcceptanceStatuses))
        {
            return Results.BadRequest(new { message = "Acceptance status must be open, contacted, accepted, or declined." });
        }

        if (!IsAllowedCandidateChoice(request.ClipEmbedStatus, CandidateClipEmbedStatuses))
        {
            return Results.BadRequest(new { message = "Clip embed status must be unchecked, embeddable, link_only, or blocked." });
        }

        if (request.AcceptanceNote?.Trim().Length > MaxCandidateAcceptanceNoteLength)
        {
            return Results.BadRequest(new { message = $"Acceptance note must stay below {MaxCandidateAcceptanceNoteLength} characters." });
        }

        var clipUrl = request.ClipCompilationUrl?.Trim();
        if (!string.IsNullOrWhiteSpace(clipUrl))
        {
            if (clipUrl.Length > MaxCandidateClipUrlLength)
            {
                return Results.BadRequest(new { message = $"Compilation link must stay below {MaxCandidateClipUrlLength} characters." });
            }

            if (!Uri.TryCreate(clipUrl, UriKind.Absolute, out var uri) || uri.Scheme is not ("http" or "https"))
            {
                return Results.BadRequest(new { message = "Compilation link must be a valid http(s) URL." });
            }
        }

        if (request.ClipCompilationTitle?.Trim().Length > MaxCandidateClipTitleLength)
        {
            return Results.BadRequest(new { message = $"Compilation title must stay below {MaxCandidateClipTitleLength} characters." });
        }

        if (request.ClipCompilationPlatform?.Trim().Length > MaxCandidateClipPlatformLength)
        {
            return Results.BadRequest(new { message = $"Compilation platform must stay below {MaxCandidateClipPlatformLength} characters." });
        }

        return null;
    }

    private static bool IsAllowedCandidateChoice(string? value, IReadOnlyCollection<string> allowedValues)
    {
        var normalized = value?.Trim();
        return string.IsNullOrWhiteSpace(normalized) || allowedValues.Contains(normalized, StringComparer.OrdinalIgnoreCase);
    }
}
