using GameList.Contracts; using GameList.Data; using GameList.Domain; using Microsoft.EntityFrameworkCore; namespace GameList.Endpoints; internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFactory httpFactory) { public async Task GetMineAsync(Player player) { var mine = await db.Suggestions .AsNoTracking() .Where(s => s.PlayerId == player.Id) .Select(s => new { s.Id, s.PlayerId, s.Name, s.Genre, s.Description, s.ScreenshotUrl, s.YoutubeUrl, s.GameUrl, s.CreatedAt, s.MinPlayers, s.MaxPlayers, s.ParentSuggestionId }) .ToListAsync(); var ordered = mine .OrderBy(s => s.CreatedAt) .Select(s => new SuggestionDto(s.Id, s.Name, s.Genre, s.Description, s.ScreenshotUrl, s.YoutubeUrl, s.GameUrl, s.MinPlayers, s.MaxPlayers, s.ParentSuggestionId)); return Results.Ok(ordered); } public async Task CreateAsync(Player player, SuggestionRequest request) { var validationError = await SuggestionValidator.ValidateAsync(request, httpFactory); if (validationError is not null) return EndpointHelpers.BadRequestError(validationError); var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id); var usingJoker = phase == Phase.Vote && player.HasJoker; if (phase != Phase.Suggest && !usingJoker) return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase); if (string.IsNullOrWhiteSpace(player.DisplayName)) return EndpointHelpers.BadRequestError("Set a display name before submitting suggestions."); var existingCount = await db.Suggestions.CountAsync(s => s.PlayerId == player.Id); if (!usingJoker && existingCount >= 5) return EndpointHelpers.BadRequestError("You have reached the 5 suggestion limit."); var suggestion = new Suggestion { PlayerId = player.Id, Name = request.Name.Trim(), Genre = EndpointHelpers.TrimTo(request.Genre, 50), Description = EndpointHelpers.TrimTo(request.Description, 500), ScreenshotUrl = EndpointHelpers.TrimTo(request.ScreenshotUrl, 2048), YoutubeUrl = EndpointHelpers.TrimTo(request.YoutubeUrl, 2048), GameUrl = EndpointHelpers.TrimTo(request.GameUrl, 2048), MinPlayers = request.MinPlayers, MaxPlayers = request.MaxPlayers }; await using var tx = await db.Database.BeginTransactionAsync(); db.Suggestions.Add(suggestion); if (usingJoker) { player.HasJoker = false; await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.VotesFinal, false)); } await db.SaveChangesAsync(); await tx.CommitAsync(); return Results.Created($"/api/suggestions/{suggestion.Id}", new SuggestionCreatedResponse(suggestion.Id)); } public async Task DeleteAsync(Player player, bool isAdmin, int suggestionId) { if (!isAdmin) { var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id); if (phase != Phase.Suggest) return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase); } var suggestion = isAdmin ? await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId) : await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId && s.PlayerId == player.Id); if (suggestion == null) return EndpointHelpers.NotFoundError("Suggestion not found."); await using var tx = await db.Database.BeginTransactionAsync(); await db.Suggestions .Where(s => s.ParentSuggestionId == suggestion.Id) .ExecuteUpdateAsync(s => s.SetProperty(x => x.ParentSuggestionId, (int?)null)); await db.Votes.Where(v => v.SuggestionId == suggestion.Id).ExecuteDeleteAsync(); db.Suggestions.Remove(suggestion); await db.SaveChangesAsync(); await tx.CommitAsync(); return Results.NoContent(); } public async Task UpdateAsync(Player player, bool isAdmin, int suggestionId, SuggestionRequest request) { var validationError = await SuggestionValidator.ValidateAsync(request, httpFactory); if (validationError is not null) return EndpointHelpers.BadRequestError(validationError); var suggestion = await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId); if (suggestion == null) return EndpointHelpers.NotFoundError("Suggestion not found."); if (!isAdmin) { if (suggestion.PlayerId != player.Id) return EndpointHelpers.UnauthorizedError(); var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id); if (phase == Phase.Results) return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase); if (phase == Phase.Suggest) { suggestion.Name = request.Name.Trim(); } else if (phase != Phase.Vote) { return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase); } ApplyEditableFields(suggestion, request); } else { suggestion.Name = request.Name.Trim(); ApplyEditableFields(suggestion, request); } await db.SaveChangesAsync(); return Results.Ok(new SuggestionUpdatedResponse( suggestion.Id, suggestion.Name, suggestion.Genre, suggestion.Description, suggestion.ScreenshotUrl, suggestion.YoutubeUrl, suggestion.GameUrl, suggestion.MinPlayers, suggestion.MaxPlayers )); } public async Task GetAllAsync(Player player) { var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, player.Id); if (phase < Phase.Vote) return EndpointHelpers.PhaseMismatch(Phase.Vote, phase); var all = await db.Suggestions .AsNoTracking() .Include(s => s.Player) .Select(s => new { s.Id, s.Name, s.Genre, s.Description, s.ScreenshotUrl, s.YoutubeUrl, s.GameUrl, s.MinPlayers, s.MaxPlayers, Author = s.Player!.DisplayName, s.CreatedAt, s.ParentSuggestionId, IsOwner = s.PlayerId == player.Id }) .ToListAsync(); var rootIndex = EndpointHelpers.BuildLinkRoots(all.Select(s => (s.Id, s.ParentSuggestionId))); var nameLookup = all.ToDictionary(s => s.Id, s => s.Name); var ordered = all.OrderBy(s => s.CreatedAt).Select(s => { var linkedIds = EndpointHelpers.LinkedIdsFor(s.Id, rootIndex).Where(id => id != s.Id).ToList(); return new { s.Id, s.Name, s.Genre, s.Description, s.ScreenshotUrl, s.YoutubeUrl, s.GameUrl, s.MinPlayers, s.MaxPlayers, s.Author, s.ParentSuggestionId, s.IsOwner, LinkedIds = linkedIds, LinkedTitles = linkedIds.Where(nameLookup.ContainsKey).Select(id => nameLookup[id]).ToList() }; }); return Results.Ok(ordered); } private static void ApplyEditableFields(Suggestion suggestion, SuggestionRequest request) { suggestion.Genre = EndpointHelpers.TrimTo(request.Genre, 50); suggestion.Description = EndpointHelpers.TrimTo(request.Description, 500); suggestion.ScreenshotUrl = EndpointHelpers.TrimTo(request.ScreenshotUrl, 2048); suggestion.YoutubeUrl = EndpointHelpers.TrimTo(request.YoutubeUrl, 2048); suggestion.GameUrl = EndpointHelpers.TrimTo(request.GameUrl, 2048); suggestion.MinPlayers = request.MinPlayers; suggestion.MaxPlayers = request.MaxPlayers; } }