using Backend.Common; using Backend.Contracts; using Backend.Domain; using Backend.Services; using Microsoft.AspNetCore.Http.Extensions; namespace Backend.Endpoints; public static partial class PublicEndpoints { private readonly record struct SubmitterIdResolution(string? SubmitterId, IResult? Result); private readonly record struct PublicWriteSeasonResolution(Season? Season, IResult? Result); private sealed record PublicCandidateClip( int? CategoryId, int? CandidateId, string Creator, string ClipUrl, string Title, string Platform, DateTimeOffset CreatedAt, DateTimeOffset? ReviewedAt); private static async Task ResolveSubmitterIdAsync( HttpContext context, string? fallbackTwitchUserId, IUserSessionService userSessionService) { var session = await userSessionService.ResolveSessionAsync(context, context.RequestAborted); if (session is null) { return new SubmitterIdResolution( null, Results.Json( new { message = "A logged in user is required to submit this action." }, statusCode: StatusCodes.Status401Unauthorized)); } var submittedTwitchUserId = NormalizeSubmittedTwitchUserId(fallbackTwitchUserId); if (submittedTwitchUserId is not null && !string.Equals(submittedTwitchUserId, session.TwitchUserId, StringComparison.OrdinalIgnoreCase)) { return new SubmitterIdResolution( null, Results.BadRequest(new { message = "Submitted user identity does not match the active session." })); } return new SubmitterIdResolution(session.TwitchUserId, null); } private static string? NormalizeSubmittedTwitchUserId(string? twitchUserId) { var normalized = twitchUserId?.Trim(); return string.IsNullOrWhiteSpace(normalized) ? null : normalized; } private static PublicWriteSeasonResolution EnsurePublicWriteSeason( Season? season, params string[] allowedPhaseKeys) { if (season is null) { return new PublicWriteSeasonResolution(null, Results.BadRequest(new { message = "The selected season does not exist." })); } if (!season.IsCurrent) { return new PublicWriteSeasonResolution( null, Results.BadRequest(new { message = "Submissions are only allowed for the active season." })); } var currentPhaseKey = SeasonMappings.NormalizePhaseKey(season.CurrentPhase); if (!allowedPhaseKeys.Contains(currentPhaseKey, StringComparer.OrdinalIgnoreCase)) { var phaseLabel = DescribePublicPhase(currentPhaseKey); return new PublicWriteSeasonResolution( null, Results.BadRequest(new { message = $"This action is not available during the current season phase ({phaseLabel}).", })); } return new PublicWriteSeasonResolution(season, null); } private static bool TryNormalizeExternalUrl(string? rawUrl, out string normalizedUrl) { normalizedUrl = string.Empty; if (string.IsNullOrWhiteSpace(rawUrl)) { return false; } if (!Uri.TryCreate(rawUrl.Trim(), UriKind.Absolute, out var uri)) { return false; } if (uri.Scheme is not ("http" or "https")) { return false; } normalizedUrl = uri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped); return true; } private static string? ResolveClipPlatform(string clipUrl) { if (!Uri.TryCreate(clipUrl, UriKind.Absolute, out var uri)) { return null; } var host = uri.Host.ToLowerInvariant(); if (host is "twitch.tv" or "www.twitch.tv" or "clips.twitch.tv") { return "Twitch"; } if (host is "youtube.com" or "www.youtube.com" or "m.youtube.com" or "youtu.be") { return "YouTube"; } return null; } private static bool ShouldExposePublicCategory(string phaseKey, int candidateCount) => string.Equals(phaseKey, "nomination", StringComparison.OrdinalIgnoreCase) || candidateCount > 0; private static Dictionary BuildCandidateClipLookup(IEnumerable clips) => clips .Where(clip => clip.CandidateId is not null) .GroupBy(clip => clip.CandidateId!.Value) .ToDictionary( grouping => grouping.Key, grouping => grouping .OrderByDescending(clip => clip.ReviewedAt ?? clip.CreatedAt) .First()); private static Dictionary BuildCreatorClipLookup(IEnumerable clips) => clips .Where(clip => clip.CategoryId is not null && !string.IsNullOrWhiteSpace(clip.Creator)) .GroupBy(clip => BuildCandidateClipLookupKey(clip.CategoryId!.Value, clip.Creator)) .Where(grouping => !string.IsNullOrWhiteSpace(grouping.Key)) .ToDictionary( grouping => grouping.Key, grouping => grouping .OrderByDescending(clip => clip.ReviewedAt ?? clip.CreatedAt) .First()); private static PublicCandidateClip? ResolveCandidateClip( Candidate candidate, IReadOnlyDictionary clipsByCandidateId, IReadOnlyDictionary clipsByCreatorKey) { if (clipsByCandidateId.TryGetValue(candidate.Id, out var directClip)) { return directClip; } foreach (var key in BuildCandidateClipLookupKeys(candidate)) { if (clipsByCreatorKey.TryGetValue(key, out var fallbackClip)) { return fallbackClip; } } return null; } private static IEnumerable BuildCandidateClipLookupKeys(Candidate candidate) { yield return BuildCandidateClipLookupKey(candidate.CategoryId, candidate.DisplayName); yield return BuildCandidateClipLookupKey(candidate.CategoryId, candidate.ChannelSlug); yield return BuildCandidateClipLookupKey(candidate.CategoryId, candidate.ChannelSlug.TrimStart('@')); } private static string BuildCandidateClipLookupKey(int categoryId, string value) { var key = NormalizeCandidateClipKey(value); return string.IsNullOrWhiteSpace(key) ? string.Empty : $"{categoryId}:{key}"; } private static string NormalizeCandidateClipKey(string value) { var normalizedCharacters = value .Trim() .TrimStart('@') .ToLowerInvariant() .Where(char.IsLetterOrDigit) .ToArray(); return new string(normalizedCharacters); } private static string DescribePublicPhase(string phaseKey) => phaseKey switch { "nomination" => "nomination", "voting" => "voting", "review" => "review", "show" => "show", _ => "current", }; private static IResult? ValidateCreateNominationRequest(CreateNominationRequest? request) { if (request is null) { return Results.BadRequest(new { message = "A nomination request body is required." }); } if (request.Year <= 0) { return Results.BadRequest(new { message = "A valid season year is required." }); } if ((request.Nominations?.Length ?? 0) == 0 && (request.Nominees?.Length ?? 0) == 0) { return Results.BadRequest(new { message = "A nomination request must include at least one stream link." }); } return null; } private static IResult? ValidateCreateVoteRequest(CreateVoteRequest? request) { if (request is null) { return Results.BadRequest(new { message = "A vote request body is required." }); } if (request.SeasonId <= 0) { return Results.BadRequest(new { message = "A valid season id is required." }); } if (request.Entries is null || request.Entries.Length == 0) { return Results.BadRequest(new { message = "At least one vote entry is required." }); } if (request.Entries.Any(item => item.CategoryId <= 0 || item.CandidateId <= 0)) { return Results.BadRequest(new { message = "Each vote entry requires a valid category and candidate." }); } return null; } private static IResult? ValidateCreateClipRequest(CreateClipRequest? request) { if (request is null) { return Results.BadRequest(new { message = "A clip request body is required." }); } if (request.Year <= 0) { return Results.BadRequest(new { message = "A valid season year is required." }); } if (request.CategoryId is <= 0 || request.CandidateId is <= 0) { return Results.BadRequest(new { message = "Category and candidate ids must be positive when provided." }); } if (string.IsNullOrWhiteSpace(request.ClipUrl)) { return Results.BadRequest(new { message = "A valid http(s) clip link is required." }); } return null; } }