using System.Collections.Concurrent; namespace GameList.Endpoints; internal static class SuggestionValidator { private static readonly ConcurrentDictionary ImageReachabilityCache = new(StringComparer.OrdinalIgnoreCase); private static readonly TimeSpan ReachableCacheTtl = TimeSpan.FromMinutes(15); private static readonly TimeSpan UnreachableCacheTtl = TimeSpan.FromMinutes(2); public static async Task ValidateAsync(SuggestionInput input, IHttpClientFactory httpFactory, bool shouldValidateImageReachability = true) { if (string.IsNullOrWhiteSpace(input.Name) || input.Name.Length > 100) return "Name is required and must be <= 100 characters."; if (!EndpointHelpers.IsValidImageUrl(input.ScreenshotUrl)) return "Screenshot URL must be http(s) and end with an image file extension."; if (shouldValidateImageReachability && !await IsReachableImageCachedAsync(input.ScreenshotUrl, httpFactory)) return "Screenshot URL could not be validated as an image. Use a public image link (http/https, no redirects, max 5 MB)."; if (!EndpointHelpers.IsValidHttpUrl(input.GameUrl)) return "Game URL must be http or https."; if (!EndpointHelpers.IsValidHttpUrl(input.YoutubeUrl)) return "YouTube URL must be http or https."; return ValidatePlayers(input.MinPlayers, input.MaxPlayers); } private static async Task IsReachableImageCachedAsync(string? url, IHttpClientFactory httpFactory) { if (string.IsNullOrWhiteSpace(url)) return true; var normalized = url.Trim(); if (ImageReachabilityCache.TryGetValue(normalized, out var cached) && cached.ExpiresAt > DateTimeOffset.UtcNow) return cached.Reachable; var reachable = await EndpointHelpers.IsReachableImageAsync(normalized, httpFactory); var ttl = reachable ? ReachableCacheTtl : UnreachableCacheTtl; ImageReachabilityCache[normalized] = (reachable, DateTimeOffset.UtcNow.Add(ttl)); return reachable; } private static string? ValidatePlayers(int? minPlayers, int? maxPlayers) { if (minPlayers is null && maxPlayers is null) return null; if (minPlayers is < 1 or > 32) return "Min players must be between 1 and 32."; if (maxPlayers is < 1 or > 32) return "Max players must be between 1 and 32."; if (minPlayers is null || maxPlayers is null) return "Provide both min and max players."; if (minPlayers > maxPlayers) return "Min players cannot exceed max players."; return null; } }