using FlatRender.IdentitySvc.Application.Services.Interfaces; using FlatRender.IdentitySvc.Domain.Entities; using FlatRender.IdentitySvc.Domain.Enums; using FlatRender.IdentitySvc.Infrastructure.Data; using FlatRender.IdentitySvc.Models.Responses; using Microsoft.EntityFrameworkCore; namespace FlatRender.IdentitySvc.Application.Services; public class GamificationService(IdentityDbContext db) : IGamificationService { public async Task> GetActiveQuestsAsync(Guid userId, Guid tenantId) { var quests = await db.Quests .Where(q => q.IsActive && (q.TenantId == null || q.TenantId == tenantId) && (q.StartsAt == null || q.StartsAt <= DateTime.UtcNow) && (q.ExpiresAt == null || q.ExpiresAt > DateTime.UtcNow)) .OrderBy(q => q.OrderValue) .ToListAsync(); var progressMap = await db.UserQuestProgresses .Where(p => p.UserId == userId && quests.Select(q => q.Id).Contains(p.QuestId)) .ToDictionaryAsync(p => p.QuestId, p => p); return quests.Select(q => { progressMap.TryGetValue(q.Id, out var progress); return new QuestResponse( q.Id, q.Title, q.Challenge, q.Why, q.Hint, q.Icon, q.QuestType.ToString(), q.TargetCount, progress?.CurrentCount ?? 0, progress?.IsCompleted ?? false, progress?.PrizeClaimed ?? false, q.PrizeType.ToString(), q.PrizeAmount, q.ExpiresAt ); }).ToList(); } public async Task ClaimQuestPrizeAsync(Guid userId, Guid questId) { var progress = await db.UserQuestProgresses .FirstOrDefaultAsync(p => p.UserId == userId && p.QuestId == questId && p.IsCompleted && !p.PrizeClaimed) ?? throw new InvalidOperationException("Quest not completed or prize already claimed"); var quest = await db.Quests.FindAsync(questId) ?? throw new KeyNotFoundException("Quest not found"); await ApplyPrizeAsync(userId, quest.PrizeType, quest.PrizeAmount); progress.PrizeClaimed = true; progress.PrizeClaimedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } public async Task> GetEarnedGiftsAsync(Guid userId) { var gifts = await db.EarnedGifts .Include(eg => eg.Gift) .Where(eg => eg.UserId == userId && !eg.IsUsed && (eg.ExpiresAt == null || eg.ExpiresAt > DateTime.UtcNow)) .OrderByDescending(eg => eg.EarnedAt) .ToListAsync(); return gifts.Select(eg => new EarnedGiftResponse( eg.Id, eg.GiftId, eg.Gift.Name, eg.Gift.Description, eg.Gift.PrizeType.ToString(), eg.Gift.Value, eg.Gift.Unit, eg.EarnedAt, eg.ExpiresAt, eg.IsUsed )).ToList(); } public async Task UseEarnedGiftAsync(Guid userId, Guid earnedGiftId) { var earned = await db.EarnedGifts .Include(eg => eg.Gift) .FirstOrDefaultAsync(eg => eg.Id == earnedGiftId && eg.UserId == userId && !eg.IsUsed && (eg.ExpiresAt == null || eg.ExpiresAt > DateTime.UtcNow)) ?? throw new InvalidOperationException("Earned gift not found or already used"); await ApplyPrizeAsync(userId, earned.Gift.PrizeType, earned.Gift.Value); earned.IsUsed = true; earned.UsedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } public async Task IncrementQuestProgressAsync(Guid userId, Guid tenantId, string targetEvent) { var matchingQuests = await db.Quests .Where(q => q.IsActive && q.TargetEvent == targetEvent && (q.TenantId == null || q.TenantId == tenantId) && (q.ExpiresAt == null || q.ExpiresAt > DateTime.UtcNow)) .ToListAsync(); foreach (var quest in matchingQuests) { var today = DateOnly.FromDateTime(DateTime.UtcNow); DateOnly? periodStart = quest.QuestType switch { QuestType.Daily => today, QuestType.Weekly => today.AddDays(-(int)DateTime.UtcNow.DayOfWeek), _ => null }; var progress = await db.UserQuestProgresses .FirstOrDefaultAsync(p => p.UserId == userId && p.QuestId == quest.Id && p.PeriodStart == periodStart); if (progress == null) { progress = new UserQuestProgress { UserId = userId, QuestId = quest.Id, PeriodStart = periodStart, }; db.UserQuestProgresses.Add(progress); } if (progress.IsCompleted) continue; progress.CurrentCount++; if (progress.CurrentCount >= quest.TargetCount) { progress.IsCompleted = true; progress.CompletedAt = DateTime.UtcNow; } } await db.SaveChangesAsync(); } private async Task ApplyPrizeAsync(Guid userId, PrizeType prizeType, long amount) { var user = await db.Users.FindAsync(userId) ?? throw new KeyNotFoundException("User not found"); switch (prizeType) { case PrizeType.Balance: user.BalanceMinor += amount; break; case PrizeType.RenderSeconds: user.UserDailyFreeChargeSec += (int)amount; break; case PrizeType.LoyaltyPoints: user.LoyaltyScore += (int)amount; break; // StorageGB, Plan, Discount require more complex handling (not inline) } user.UpdatedAt = DateTime.UtcNow; } }