using Microsoft.EntityFrameworkCore; using SlipItIn.Server.Data; using SlipItIn.Shared.DTOs; using SlipItIn.Shared.Models; using System.Security.Cryptography; namespace SlipItIn.Server.Services; public class GameService : IGameService { private readonly IDbContextFactory _contextFactory; private readonly ILogger _logger; public GameService(IDbContextFactory contextFactory, ILogger logger) { _contextFactory = contextFactory; _logger = logger; } public async Task CreateGameAsync(int hostUserId, string? connectionId = null) { using var context = _contextFactory.CreateDbContext(); var host = await context.Users.FirstOrDefaultAsync(u => u.Id == hostUserId) ?? throw new InvalidOperationException("Host not found"); var lobbyCode = await GenerateUniqueLobbyCodeAsync(context); var game = new Game { LobbyCode = lobbyCode, HostId = host.Id, Status = GameStatus.Lobby }; // Host wird automatisch als erster Spieler hinzugefügt var hostPlayer = new Player { UserId = host.Id, ConnectionId = connectionId }; // Spieler direkt zur Liste des Spiels hinzufügen game.Players.Add(hostPlayer); // Es reicht, nur das Game hinzuzufügen — EF Core fügt den Player automatisch mit hinzu context.Games.Add(game); await context.SaveChangesAsync(); return game; } public async Task JoinGameAsync(string lobbyCode, int userId, string? connectionId = null) { using var context = _contextFactory.CreateDbContext(); var game = await context.Games .Include(g => g.Players) .FirstOrDefaultAsync(g => g.LobbyCode == lobbyCode && g.Status == GameStatus.Lobby) ?? throw new InvalidOperationException("Game not found or already started"); if (game.Players.Count >= game.MaxPlayers) throw new InvalidOperationException("Game is full"); var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId) ?? throw new InvalidOperationException("User not found"); // Prüfen ob User bereits im Spiel ist if (game.Players.Any(p => p.UserId == userId)) throw new InvalidOperationException("User already in game"); var player = new Player { UserId = user.Id, GameId = game.Id, ConnectionId = connectionId }; context.Players.Add(player); await context.SaveChangesAsync(); return game; } public async Task StartGameAsync(int gameId) { using var context = _contextFactory.CreateDbContext(); var game = await context.Games .Include(g => g.Players) .FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new InvalidOperationException("Game not found"); if (game.Status != GameStatus.Lobby) throw new InvalidOperationException("Game has already started or ended"); if (game.Players.Count < 2) throw new InvalidOperationException("At least 2 players are required to start the game"); game.Status = GameStatus.InProgress; game.StartedAt = DateTime.UtcNow; var round = new GameRound { GameId = gameId, RoundNumber = 1 }; context.GameRounds.Add(round); await context.SaveChangesAsync(); return game; } public async Task SetPlayerReadyAsync(int gameId, int playerId) { using var context = _contextFactory.CreateDbContext(); var game = await context.Games.FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new InvalidOperationException("Game not found"); if (game.Status != GameStatus.Lobby) throw new InvalidOperationException("Game is not in lobby status"); var player = await context.Players.FirstOrDefaultAsync(p => p.Id == playerId && p.GameId == gameId) ?? throw new InvalidOperationException("Player not found in this game"); player.IsReady = true; await context.SaveChangesAsync(); return game; } public async Task> DealCardsAsync(int gameId) { using var context = _contextFactory.CreateDbContext(); var players = await context.Players.Where(p => p.GameId == gameId).ToListAsync(); var currentRound = await context.GameRounds .Where(gr => gr.GameId == gameId && gr.Status == RoundStatus.Waiting) .FirstOrDefaultAsync() ?? throw new InvalidOperationException("No active round found"); var allPhrases = await context.Phrases.Where(p => p.IsActive).ToListAsync(); if (allPhrases.Count < 5) throw new InvalidOperationException("Not enough phrases in database"); var dealtCards = new List(); foreach (var player in players) { var selectedPhrases = allPhrases.OrderBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue)).Take(5); foreach (var phrase in selectedPhrases) { var card = new PlayerCard { PlayerId = player.Id, PhraseId = phrase.Id, GameRoundId = currentRound.Id, IsUsed = false }; context.PlayerCards.Add(card); dealtCards.Add(card); } } currentRound.Status = RoundStatus.Active; currentRound.StartedAt = DateTime.UtcNow; await context.SaveChangesAsync(); return dealtCards; } public async Task SubmitSlipAsync(int gameId, int playerId, int cardId) { using var context = _contextFactory.CreateDbContext(); var game = await context.Games.FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new InvalidOperationException("Game not found"); if (game.Status != GameStatus.InProgress) throw new InvalidOperationException("Game is not in progress"); var card = await context.PlayerCards .Include(pc => pc.GameRound) .FirstOrDefaultAsync(pc => pc.Id == cardId) ?? throw new InvalidOperationException("Card not found"); if (card.PlayerId != playerId) throw new UnauthorizedAccessException("Card does not belong to player"); if (card.GameRound.GameId != gameId) throw new InvalidOperationException("Card does not belong to this game"); if (card.GameRound.Status != RoundStatus.Active) throw new InvalidOperationException("Round is not active"); if (card.IsUsed) throw new InvalidOperationException("Card has already been used"); card.IsUsed = true; await context.SaveChangesAsync(); return true; } public async Task CreateChallengeAsync(int gameId, int challengingPlayerId, int targetCardId) { using var context = _contextFactory.CreateDbContext(); var currentRound = await context.GameRounds .Where(gr => gr.GameId == gameId && gr.Status == RoundStatus.Active) .FirstOrDefaultAsync() ?? throw new InvalidOperationException("No active round found"); var challengerBelongsToGame = await context.Players .AnyAsync(p => p.Id == challengingPlayerId && p.GameId == gameId); if (!challengerBelongsToGame) throw new UnauthorizedAccessException("Challenger does not belong to this game"); var targetCard = await context.PlayerCards .FirstOrDefaultAsync(pc => pc.Id == targetCardId) ?? throw new InvalidOperationException("Target card not found"); if (targetCard.GameRoundId != currentRound.Id) throw new InvalidOperationException("Card does not belong to the active round"); if (targetCard.PlayerId == challengingPlayerId) throw new InvalidOperationException("Cannot challenge your own card"); if (!targetCard.IsUsed) throw new InvalidOperationException("Card has not been played yet"); var pendingExists = await context.SlipChallenges .AnyAsync(c => c.TargetCardId == targetCardId && c.Status == ChallengeStatus.Pending); if (pendingExists) throw new InvalidOperationException("A challenge for this card is already pending"); var challenge = new SlipChallenge { GameRoundId = currentRound.Id, ChallengingPlayerId = challengingPlayerId, TargetPlayerId = targetCard.PlayerId, TargetCardId = targetCardId, Status = ChallengeStatus.Pending }; context.SlipChallenges.Add(challenge); await context.SaveChangesAsync(); return challenge; } public async Task ResolveChallengeAsync(int challengeId, bool approved) { using var context = _contextFactory.CreateDbContext(); var challenge = await context.SlipChallenges .Include(sc => sc.GameRound) .Include(sc => sc.TargetCard) .Include(sc => sc.ChallengingPlayer) .Include(sc => sc.TargetPlayer) .FirstOrDefaultAsync(sc => sc.Id == challengeId) ?? throw new InvalidOperationException("Challenge not found"); if (challenge.Status != ChallengeStatus.Pending) throw new InvalidOperationException("Challenge has already been resolved"); // approved = Beschuldigung stimmt (Phrase war ein unberechtigtes SlipIn des Beschuldigten) // rejected = Beschuldigung stimmt nicht (Phrase wurde regelkonform gespielt) if (approved) { // Berechtigte Beschuldigung: Beschuldigter muss die Phrase an den Beschuldiger abgeben challenge.TargetCard.PlayerId = challenge.ChallengingPlayerId; challenge.ChallengingPlayer.Score += 1; challenge.TargetPlayer.FailedSlips += 1; } else { // Falsche Beschuldigung: Phrase bleibt beim Beschuldigten challenge.TargetPlayer.SuccessfulSlips += 1; challenge.TargetPlayer.Score += 1; challenge.ChallengingPlayer.FailedSlips += 1; } challenge.Status = approved ? ChallengeStatus.Approved : ChallengeStatus.Rejected; challenge.ResolvedAt = DateTime.UtcNow; await context.SaveChangesAsync(); return challenge; } public async Task GetGameStateAsync(int gameId) { using var context = _contextFactory.CreateDbContext(); var game = await context.Games .Include(g => g.Players) .ThenInclude(p => p.User) .Include(g => g.Players) .ThenInclude(p => p.Cards) .Include(g => g.Rounds) .FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new InvalidOperationException("Game not found"); var playerInfos = game.Players.Select(p => new PlayerInfoDto { PlayerId = p.Id, Username = p.User.Username, Score = p.Score, IsReady = p.IsReady, CardCount = p.Cards.Count(c => !c.IsUsed) // Nur Anzahl, nicht die Karten selbst! }).ToList(); var currentRound = game.Rounds .OrderByDescending(r => r.RoundNumber) .FirstOrDefault(); var roundDurationSeconds = Math.Clamp(game.RoundDurationSeconds, 30, 60); var roundTimeRemaining = roundDurationSeconds; if (currentRound?.Status == RoundStatus.Active) { var elapsedSeconds = (int)(DateTime.UtcNow - currentRound.StartedAt).TotalSeconds; roundTimeRemaining = Math.Max(0, roundDurationSeconds - elapsedSeconds); } return new GameStateDto { GameId = game.Id, LobbyCode = game.LobbyCode, Status = game.Status.ToString(), Players = playerInfos, CurrentRound = currentRound?.RoundNumber ?? 0, RoundTimeRemaining = roundTimeRemaining }; } public async Task GetPlayerHandAsync(int playerId) { using var context = _contextFactory.CreateDbContext(); // Nur Karten der aktuell aktiven Runde zurückgeben — sonst wächst die Hand // über Runden hinweg unbegrenzt und enthält veraltete Karten. var activeRoundIds = await context.GameRounds .Where(gr => gr.Status == RoundStatus.Active && gr.Game.Players.Any(p => p.Id == playerId)) .Select(gr => gr.Id) .ToListAsync(); var cards = await context.PlayerCards .Include(pc => pc.Phrase) .Where(pc => pc.PlayerId == playerId && activeRoundIds.Contains(pc.GameRoundId)) .Select(pc => new PlayerCardDto { CardId = pc.Id, PhraseId = pc.PhraseId, Text = pc.Phrase.Text, IsUsed = pc.IsUsed }) .ToListAsync(); return new PlayerHandDto { PlayerId = playerId, Cards = cards }; } public async Task GetGameAsync(int gameId) { using var context = _contextFactory.CreateDbContext(); return await context.Games.FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new InvalidOperationException("Game not found"); } private static string GenerateLobbyCode() { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; var buffer = new char[6]; for (var i = 0; i < buffer.Length; i++) buffer[i] = chars[RandomNumberGenerator.GetInt32(chars.Length)]; return new string(buffer); } private static async Task GenerateUniqueLobbyCodeAsync(SlipItInDbContext context, int maxAttempts = 10) { for (var attempt = 0; attempt < maxAttempts; attempt++) { var code = GenerateLobbyCode(); var exists = await context.Games.AnyAsync(g => g.LobbyCode == code); if (!exists) return code; } throw new InvalidOperationException("Could not generate a unique lobby code"); } }