using System.Globalization; using System.Text; using System.Text.Json; using Backend.Contracts; using Backend.Domain; namespace Backend.Services; public sealed record SeasonSubcategoryTemplateSetting( string Name, string Slug, int SortOrder, int? ViewerRangeMin, int? ViewerRangeMax); public static class SeasonSubcategoryTemplateSettings { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); private static readonly SeasonSubcategoryTemplateSetting[] DefaultTemplates = [ new("Hidden Star", "hidden-star", 1, 1, 20), new("Rising Star", "rising-star", 2, 21, 60), new("Shining Star", "shining-star", 3, 61, null), ]; public static SeasonSubcategoryTemplateSetting[] Read(Season season, IEnumerable? fallbackCategories = null) { var stored = OnlyViewerTemplates(Parse(season.SubcategoryTemplatesJson)); if (stored.Length > 0) { return stored; } if (fallbackCategories is null) { return []; } var fallback = fallbackCategories .GroupBy(category => new { category.Name, category.ViewerRangeMin, category.ViewerRangeMax }) .Where(group => group.Key.ViewerRangeMin is not null || group.Key.ViewerRangeMax is not null) .Select(group => { var first = group.OrderBy(item => item.SortOrder).First(); return Normalize(new SeasonSubcategoryTemplateSetting( first.Name, ExtractTemplateSlug(first.Slug, first.GroupName), first.SortOrder, first.ViewerRangeMin, first.ViewerRangeMax)); }) .OrderBy(item => item.SortOrder) .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase) .Select((item, index) => item with { SortOrder = index + 1 }) .ToArray(); return fallback.Length > 0 ? fallback : DefaultTemplates; } public static string Serialize(IEnumerable templates) => JsonSerializer.Serialize( templates.Select(Normalize) .OrderBy(item => item.SortOrder) .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase), JsonOptions); public static SeasonSubcategoryTemplateSetting[] Normalize(IEnumerable? templates) => (templates ?? []) .Select(template => Normalize(new SeasonSubcategoryTemplateSetting( template.Name, template.Slug, template.SortOrder, template.ViewerRangeMin, template.ViewerRangeMax))) .OrderBy(item => item.SortOrder) .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase) .Select((item, index) => item with { SortOrder = index + 1 }) .ToArray(); public static AdminSubcategoryTemplateDto[] ToDtos(IEnumerable templates) => templates .OrderBy(item => item.SortOrder) .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase) .Select(item => new AdminSubcategoryTemplateDto( item.Name, item.Slug, item.SortOrder, item.ViewerRangeMin, item.ViewerRangeMax)) .ToArray(); public static bool MatchesTemplate(Category category, IEnumerable templates) { var categoryTemplateSlug = ExtractTemplateSlug(category.Slug, category.GroupName); return templates.Any(template => string.Equals(category.Name, template.Name, StringComparison.OrdinalIgnoreCase) || string.Equals(categoryTemplateSlug, template.Slug, StringComparison.OrdinalIgnoreCase) || category.Slug.EndsWith($"-{template.Slug}", StringComparison.OrdinalIgnoreCase)); } private static SeasonSubcategoryTemplateSetting[] Parse(string? json) { if (string.IsNullOrWhiteSpace(json)) { return []; } try { return JsonSerializer.Deserialize(json, JsonOptions)? .Select(Normalize) .OrderBy(item => item.SortOrder) .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase) .Select((item, index) => item with { SortOrder = index + 1 }) .ToArray() ?? []; } catch (JsonException) { return []; } } private static SeasonSubcategoryTemplateSetting Normalize(SeasonSubcategoryTemplateSetting template) { var name = template.Name.Trim(); var slug = Slugify(template.Slug); if (string.IsNullOrWhiteSpace(slug)) { slug = Slugify(name); } return template with { Name = name, Slug = slug, SortOrder = Math.Clamp(template.SortOrder, 1, 99), ViewerRangeMin = NormalizeNullableNumber(template.ViewerRangeMin), ViewerRangeMax = NormalizeNullableNumber(template.ViewerRangeMax), }; } private static int? NormalizeNullableNumber(int? value) => value is null ? null : Math.Clamp(value.Value, 0, 100000); private static SeasonSubcategoryTemplateSetting[] OnlyViewerTemplates(IEnumerable templates) { var items = templates .Select(Normalize) .Where(item => item.ViewerRangeMin is not null || item.ViewerRangeMax is not null) .GroupBy(item => item.Slug, StringComparer.OrdinalIgnoreCase) .Select(group => group.OrderBy(item => item.SortOrder).First()) .OrderBy(item => item.SortOrder) .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase) .Select((item, index) => item with { SortOrder = index + 1 }) .ToArray(); return items.Length > 0 ? items : []; } private static string ExtractTemplateSlug(string categorySlug, string groupName) { var groupSlug = Slugify(groupName); var slug = categorySlug.Trim().ToLowerInvariant(); var prefix = string.IsNullOrWhiteSpace(groupSlug) ? string.Empty : $"{groupSlug}-"; if (!string.IsNullOrWhiteSpace(prefix) && slug.StartsWith(prefix, StringComparison.Ordinal)) { return slug[prefix.Length..]; } return slug; } public static string Slugify(string? value) { var normalized = (value ?? string.Empty) .Trim() .ToLowerInvariant() .Normalize(NormalizationForm.FormD); var builder = new StringBuilder(normalized.Length); var lastWasDash = false; foreach (var character in normalized) { if (CharUnicodeInfo.GetUnicodeCategory(character) == UnicodeCategory.NonSpacingMark) { continue; } if (char.IsLetterOrDigit(character)) { builder.Append(character); lastWasDash = false; continue; } if (!lastWasDash && builder.Length > 0) { builder.Append('-'); lastWasDash = true; } } return builder.ToString().Trim('-'); } }