using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RR3CommunityServer.Data; using RR3CommunityServer.Services; using RR3CommunityServer.Models; using static RR3CommunityServer.Data.RR3DbContext; namespace RR3CommunityServer.Controllers; [ApiController] [Route("synergy/[controller]")] public class RewardsController : ControllerBase { private readonly RR3DbContext _context; private readonly ILogger _logger; public RewardsController(RR3DbContext context, ILogger logger) { _context = context; _logger = logger; } /// /// Get daily reward status for a user /// [HttpGet("daily/{synergyId}")] public async Task GetDailyReward(string synergyId) { _logger.LogInformation("Getting daily reward for {SynergyId}", synergyId); var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId); if (user == null) { return NotFound(new { error = "User not found" }); } var today = DateTime.UtcNow.Date; var reward = await _context.DailyRewards .FirstOrDefaultAsync(r => r.UserId == user.Id && r.RewardDate.Date == today); if (reward == null) { // Create new daily reward reward = new DailyReward { UserId = user.Id, RewardDate = DateTime.UtcNow, GoldAmount = 50, // Daily gold reward CashAmount = 5000, // Daily cash reward Claimed = false, Streak = await CalculateStreak(user.Id) }; _context.DailyRewards.Add(reward); await _context.SaveChangesAsync(); } return Ok(new { available = !reward.Claimed, gold = reward.GoldAmount, cash = reward.CashAmount, streak = reward.Streak, nextRewardIn = reward.Claimed ? (24 - (DateTime.UtcNow - reward.RewardDate).TotalHours) : 0 }); } /// /// Claim daily reward /// [HttpPost("daily/{synergyId}/claim")] public async Task ClaimDailyReward(string synergyId) { _logger.LogInformation("Claiming daily reward for {SynergyId}", synergyId); var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId); if (user == null) { return NotFound(new { error = "User not found" }); } var today = DateTime.UtcNow.Date; var reward = await _context.DailyRewards .FirstOrDefaultAsync(r => r.UserId == user.Id && r.RewardDate.Date == today); if (reward == null || reward.Claimed) { return BadRequest(new { error = "No reward available to claim" }); } // Mark as claimed reward.Claimed = true; reward.ClaimedAt = DateTime.UtcNow; // Update user's gold and cash if (user.Gold == null) user.Gold = 0; if (user.Cash == null) user.Cash = 0; user.Gold += reward.GoldAmount; user.Cash += reward.CashAmount; await _context.SaveChangesAsync(); return Ok(new { success = true, goldEarned = reward.GoldAmount, cashEarned = reward.CashAmount, totalGold = user.Gold, totalCash = user.Cash, streak = reward.Streak }); } /// /// Purchase gold with real money (free in community server) /// /// /// Purchase gold - FREE in community server per EA agreement /// IMPORTANT: No real money transactions allowed! /// [HttpPost("gold/purchase")] public async Task PurchaseGold([FromBody] GoldPurchaseRequest request) { _logger.LogInformation("Processing gold purchase for {SynergyId}: {Amount} gold (FREE - community server)", request.SynergyId, request.GoldAmount); var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == request.SynergyId); if (user == null) { return NotFound(new { error = "User not found" }); } // ⚖️ EA COMPLIANCE: All gold purchases are FREE in community server! // Per EA agreement: No real money in-app purchases allowed if (user.Gold == null) user.Gold = 0; user.Gold += request.GoldAmount; // Log the purchase (for tracking, not billing) var purchase = new Purchase { SynergyId = request.SynergyId, UserId = user.Id, ItemId = $"gold_{request.GoldAmount}", Sku = request.Sku ?? $"com.ea.rr3.gold_{request.GoldAmount}", OrderId = Guid.NewGuid().ToString(), PurchaseTime = DateTime.UtcNow, Token = Guid.NewGuid().ToString(), Price = 0, // ⚖️ MUST BE 0 - EA COMPLIANCE Status = "approved" }; _context.Purchases.Add(purchase); await _context.SaveChangesAsync(); return Ok(new { success = true, goldPurchased = request.GoldAmount, totalGold = user.Gold, orderId = purchase.OrderId, message = "Gold added to your account (FREE in community server!)" }); } /// /// Get time trial events /// [HttpGet("timetrials")] public async Task GetTimeTrials() { _logger.LogInformation("Getting time trial events"); var trials = await _context.TimeTrials .Where(t => t.Active) .OrderBy(t => t.StartDate) .ToListAsync(); return Ok(new { events = trials.Select(t => new { id = t.Id, name = t.Name, track = t.TrackName, car = t.CarName, startDate = t.StartDate, endDate = t.EndDate, goldReward = t.GoldReward, cashReward = t.CashReward, targetTime = t.TargetTime, timeRemaining = (t.EndDate - DateTime.UtcNow).TotalSeconds, isActive = t.StartDate <= DateTime.UtcNow && t.EndDate >= DateTime.UtcNow }) }); } /// /// Get specific time trial details /// [HttpGet("timetrials/{trialId}")] public async Task GetTimeTrial(int trialId) { _logger.LogInformation("Getting time trial {TrialId}", trialId); var trial = await _context.TimeTrials.FindAsync(trialId); if (trial == null) { return NotFound(new { error = "Time trial not found" }); } // Get total participants var participantCount = await _context.TimeTrialResults .Where(r => r.TimeTrialId == trialId) .Select(r => r.UserId) .Distinct() .CountAsync(); // Get fastest time var fastestTime = await _context.TimeTrialResults .Where(r => r.TimeTrialId == trialId) .OrderBy(r => r.TimeSeconds) .FirstOrDefaultAsync(); var response = new { id = trial.Id, name = trial.Name, trackName = trial.TrackName, carName = trial.CarName, targetTime = trial.TargetTime, goldReward = trial.GoldReward, cashReward = trial.CashReward, startDate = trial.StartDate, endDate = trial.EndDate, active = trial.Active, timeRemaining = (trial.EndDate - DateTime.UtcNow).TotalSeconds, participants = participantCount, fastestTime = fastestTime?.TimeSeconds, fastestPlayer = fastestTime != null ? (await _context.Users.FindAsync(fastestTime.UserId))?.SynergyId : null }; return Ok(response); } /// /// Get player's time trial results /// [HttpGet("timetrials/player/{synergyId}/results")] public async Task GetPlayerTimeTrialResults(string synergyId, [FromQuery] int? trialId = null) { _logger.LogInformation("Getting time trial results for {SynergyId}", synergyId); var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId); if (user == null) { return NotFound(new { error = "User not found" }); } var query = _context.TimeTrialResults .Include(r => r.TimeTrial) .Where(r => r.UserId == user.Id); // Filter by specific trial if provided if (trialId.HasValue) { query = query.Where(r => r.TimeTrialId == trialId.Value); } var results = await query .OrderByDescending(r => r.SubmittedAt) .ToListAsync(); var response = new { synergyId = synergyId, totalAttempts = results.Count, totalGoldEarned = results.Sum(r => r.GoldEarned), totalCashEarned = results.Sum(r => r.CashEarned), targetsBeat = results.Count(r => r.BeatTarget), results = results.Select(r => new { trialId = r.TimeTrialId, trialName = r.TimeTrial?.Name, timeSeconds = r.TimeSeconds, beatTarget = r.BeatTarget, goldEarned = r.GoldEarned, cashEarned = r.CashEarned, submittedAt = r.SubmittedAt }).ToList() }; return Ok(response); } /// /// Claim time trial reward (bonus for completing) /// [HttpPost("timetrials/{trialId}/claim")] public async Task ClaimTimeTrialReward(int trialId, [FromBody] ClaimRewardRequest request) { _logger.LogInformation("Claiming time trial reward for {SynergyId}, trial {TrialId}", request.SynergyId, trialId); var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == request.SynergyId); if (user == null) { return NotFound(new { error = "User not found" }); } var trial = await _context.TimeTrials.FindAsync(trialId); if (trial == null || !trial.Active) { return NotFound(new { error = "Time trial not found or inactive" }); } // Check if player has a result for this trial var bestResult = await _context.TimeTrialResults .Where(r => r.UserId == user.Id && r.TimeTrialId == trialId) .OrderBy(r => r.TimeSeconds) .FirstOrDefaultAsync(); if (bestResult == null) { return BadRequest(new { error = "No results found for this time trial" }); } // Check if already claimed (could store in separate ClaimedRewards table) // For now, just give completion bonus int bonusGold = bestResult.BeatTarget ? 100 : 50; // Bonus for participating int bonusCash = bestResult.BeatTarget ? 10000 : 5000; user.Gold = (user.Gold ?? 0) + bonusGold; user.Cash = (user.Cash ?? 0) + bonusCash; await _context.SaveChangesAsync(); return Ok(new { success = true, bonusGold = bonusGold, bonusCash = bonusCash, totalGold = user.Gold, totalCash = user.Cash, message = bestResult.BeatTarget ? "🏆 Completion bonus claimed!" : "Thanks for participating! Keep racing!" }); } /// /// Submit time trial result /// [HttpPost("timetrials/{trialId}/submit")] public async Task SubmitTimeTrial(int trialId, [FromBody] TimeTrialSubmission submission) { _logger.LogInformation("Submitting time trial {TrialId} for {SynergyId}: {Time}s", trialId, submission.SynergyId, submission.TimeSeconds); var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == submission.SynergyId); if (user == null) { return NotFound(new { error = "User not found" }); } var trial = await _context.TimeTrials.FindAsync(trialId); if (trial == null || !trial.Active) { return NotFound(new { error = "Time trial not found or inactive" }); } // Check if beat target time bool beatTarget = submission.TimeSeconds <= trial.TargetTime; int goldEarned = beatTarget ? trial.GoldReward : 0; int cashEarned = beatTarget ? trial.CashReward : trial.CashReward / 2; // Half cash for participation // Check for personal best var personalRecord = await _context.PersonalRecords .FirstOrDefaultAsync(pr => pr.SynergyId == submission.SynergyId && pr.RecordType == "TimeTrial" && pr.RecordCategory == trialId.ToString()); bool isNewPersonalBest = false; double? previousBestTime = null; double? improvement = null; if (personalRecord == null) { // First attempt - create new personal record isNewPersonalBest = true; personalRecord = new Data.PersonalRecord { SynergyId = submission.SynergyId, RecordType = "TimeTrial", RecordCategory = trialId.ToString(), TrackName = trial.TrackName, CarName = submission.CarName, BestTimeSeconds = submission.TimeSeconds, AchievedAt = DateTime.UtcNow, TotalAttempts = 1 }; _context.PersonalRecords.Add(personalRecord); } else { // Update attempt count personalRecord.TotalAttempts++; // Check if this is a new personal best if (submission.TimeSeconds < personalRecord.BestTimeSeconds) { isNewPersonalBest = true; previousBestTime = personalRecord.BestTimeSeconds; improvement = personalRecord.BestTimeSeconds - submission.TimeSeconds; personalRecord.BestTimeSeconds = submission.TimeSeconds; personalRecord.AchievedAt = DateTime.UtcNow; personalRecord.ImprovementSeconds = improvement; personalRecord.CarName = submission.CarName; } } // Save result var result = new TimeTrialResult { UserId = user.Id, TimeTrialId = trialId, TimeSeconds = submission.TimeSeconds, SubmittedAt = DateTime.UtcNow, BeatTarget = beatTarget, GoldEarned = goldEarned, CashEarned = cashEarned }; _context.TimeTrialResults.Add(result); // Add/update leaderboard entry if personal best if (isNewPersonalBest) { var leaderboardEntry = new Data.LeaderboardEntry { SynergyId = submission.SynergyId, PlayerName = user.SynergyId, RecordType = "TimeTrial", RecordCategory = trialId.ToString(), TrackName = trial.TrackName, CarName = submission.CarName, TimeSeconds = submission.TimeSeconds, SubmittedAt = DateTime.UtcNow }; _context.LeaderboardEntries.Add(leaderboardEntry); } // Award currency if (user.Gold == null) user.Gold = 0; if (user.Cash == null) user.Cash = 0; user.Gold += goldEarned; user.Cash += cashEarned; // Bonus rewards for personal best if (isNewPersonalBest && previousBestTime.HasValue) { int bonusGold = 50; // Bonus for improving user.Gold += bonusGold; goldEarned += bonusGold; } await _context.SaveChangesAsync(); // Calculate global rank int globalRank = await _context.LeaderboardEntries .Where(e => e.RecordType == "TimeTrial" && e.RecordCategory == trialId.ToString() && e.TimeSeconds < submission.TimeSeconds) .CountAsync() + 1; // Check if this is a new global record bool isNewGlobalRecord = globalRank == 1; return Ok(new RecordSubmissionResponse { Success = true, IsNewPersonalBest = isNewPersonalBest, IsNewGlobalRecord = isNewGlobalRecord, GlobalRank = globalRank, PreviousBestTime = previousBestTime, Improvement = improvement, GoldEarned = goldEarned, CashEarned = cashEarned }); } private async Task CalculateStreak(int userId) { var rewards = await _context.DailyRewards .Where(r => r.UserId == userId && r.Claimed) .OrderByDescending(r => r.RewardDate) .ToListAsync(); int streak = 0; var currentDate = DateTime.UtcNow.Date; foreach (var reward in rewards) { if (reward.RewardDate.Date == currentDate.AddDays(-streak)) { streak++; } else { break; } } return streak; } } public class GoldPurchaseRequest { public string SynergyId { get; set; } = string.Empty; public int GoldAmount { get; set; } public string? Sku { get; set; } } public class TimeTrialSubmission { public string SynergyId { get; set; } = string.Empty; public double TimeSeconds { get; set; } public string? CarName { get; set; } } public class ClaimRewardRequest { public string SynergyId { get; set; } = string.Empty; }