using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RR3CommunityServer.Data; using RR3CommunityServer.Models; namespace RR3CommunityServer.Controllers; [ApiController] [Route("synergy/[controller]")] public class LeaderboardsController : ControllerBase { private readonly RR3DbContext _context; private readonly ILogger _logger; public LeaderboardsController(RR3DbContext context, ILogger logger) { _context = context; _logger = logger; } /// /// Get leaderboard for a specific time trial /// [HttpGet("timetrials/{trialId}")] public async Task GetTimeTrialLeaderboard(int trialId, [FromQuery] int limit = 100, [FromQuery] string? synergyId = null) { _logger.LogInformation("Getting time trial leaderboard for trial {TrialId}, limit {Limit}", trialId, limit); var trial = await _context.TimeTrials.FindAsync(trialId); if (trial == null) { return NotFound(new { error = "Time trial not found" }); } // Get all entries for this time trial, ordered by time var entries = await _context.LeaderboardEntries .Where(e => e.RecordType == "TimeTrial" && e.RecordCategory == trialId.ToString()) .OrderBy(e => e.TimeSeconds) .Take(limit) .ToListAsync(); // Convert to DTOs with rankings var leaderboard = entries.Select((entry, index) => new LeaderboardEntryDto { Rank = index + 1, PlayerName = entry.PlayerName, SynergyId = entry.SynergyId, TimeSeconds = entry.TimeSeconds, FormattedTime = FormatTime(entry.TimeSeconds), SubmittedAt = entry.SubmittedAt, CarName = entry.CarName, IsCurrentPlayer = !string.IsNullOrEmpty(synergyId) && entry.SynergyId == synergyId }).ToList(); // Find player's entry if synergyId provided LeaderboardEntryDto? playerEntry = null; if (!string.IsNullOrEmpty(synergyId)) { var playerRecord = await _context.LeaderboardEntries .Where(e => e.RecordType == "TimeTrial" && e.RecordCategory == trialId.ToString() && e.SynergyId == synergyId) .OrderBy(e => e.TimeSeconds) .FirstOrDefaultAsync(); if (playerRecord != null) { // Calculate player's rank var betterTimes = await _context.LeaderboardEntries .Where(e => e.RecordType == "TimeTrial" && e.RecordCategory == trialId.ToString() && e.TimeSeconds < playerRecord.TimeSeconds) .CountAsync(); playerEntry = new LeaderboardEntryDto { Rank = betterTimes + 1, PlayerName = playerRecord.PlayerName, SynergyId = playerRecord.SynergyId, TimeSeconds = playerRecord.TimeSeconds, FormattedTime = FormatTime(playerRecord.TimeSeconds), SubmittedAt = playerRecord.SubmittedAt, CarName = playerRecord.CarName, IsCurrentPlayer = true }; } } var response = new LeaderboardResponse { RecordType = "TimeTrial", RecordCategory = $"{trial.Name} - {trial.TrackName}", TotalEntries = await _context.LeaderboardEntries .Where(e => e.RecordType == "TimeTrial" && e.RecordCategory == trialId.ToString()) .CountAsync(), Entries = leaderboard, PlayerEntry = playerEntry }; return Ok(new SynergyResponse { resultCode = 0, data = response }); } /// /// Get leaderboard for a specific career event /// [HttpGet("career/{series}/{eventName}")] public async Task GetCareerLeaderboard(string series, string eventName, [FromQuery] int limit = 100, [FromQuery] string? synergyId = null) { _logger.LogInformation("Getting career leaderboard for {Series}/{Event}, limit {Limit}", series, eventName, limit); var entries = await _context.LeaderboardEntries .Where(e => e.RecordType == "Career" && e.RecordCategory == $"{series}/{eventName}") .OrderBy(e => e.TimeSeconds) .Take(limit) .ToListAsync(); var leaderboard = entries.Select((entry, index) => new LeaderboardEntryDto { Rank = index + 1, PlayerName = entry.PlayerName, SynergyId = entry.SynergyId, TimeSeconds = entry.TimeSeconds, FormattedTime = FormatTime(entry.TimeSeconds), SubmittedAt = entry.SubmittedAt, CarName = entry.CarName, IsCurrentPlayer = !string.IsNullOrEmpty(synergyId) && entry.SynergyId == synergyId }).ToList(); // Find player's entry LeaderboardEntryDto? playerEntry = null; if (!string.IsNullOrEmpty(synergyId)) { var playerRecord = await _context.LeaderboardEntries .Where(e => e.RecordType == "Career" && e.RecordCategory == $"{series}/{eventName}" && e.SynergyId == synergyId) .OrderBy(e => e.TimeSeconds) .FirstOrDefaultAsync(); if (playerRecord != null) { var betterTimes = await _context.LeaderboardEntries .Where(e => e.RecordType == "Career" && e.RecordCategory == $"{series}/{eventName}" && e.TimeSeconds < playerRecord.TimeSeconds) .CountAsync(); playerEntry = new LeaderboardEntryDto { Rank = betterTimes + 1, PlayerName = playerRecord.PlayerName, SynergyId = playerRecord.SynergyId, TimeSeconds = playerRecord.TimeSeconds, FormattedTime = FormatTime(playerRecord.TimeSeconds), SubmittedAt = playerRecord.SubmittedAt, CarName = playerRecord.CarName, IsCurrentPlayer = true }; } } var response = new LeaderboardResponse { RecordType = "Career", RecordCategory = $"{series} - {eventName}", TotalEntries = await _context.LeaderboardEntries .Where(e => e.RecordType == "Career" && e.RecordCategory == $"{series}/{eventName}") .CountAsync(), Entries = leaderboard, PlayerEntry = playerEntry }; return Ok(new SynergyResponse { resultCode = 0, data = response }); } /// /// Get global top 100 players (by total records count or best average time) /// [HttpGet("global/top100")] public async Task GetGlobalTop100([FromQuery] string metric = "records") { _logger.LogInformation("Getting global top 100 players by {Metric}", metric); if (metric == "records") { // Rank by total number of personal records var topPlayers = await _context.PersonalRecords .GroupBy(pr => new { pr.SynergyId }) .Select(g => new { g.Key.SynergyId, RecordCount = g.Count(), AverageTime = g.Average(pr => pr.BestTimeSeconds) }) .OrderByDescending(p => p.RecordCount) .ThenBy(p => p.AverageTime) .Take(100) .ToListAsync(); // Get player names var result = new List(); foreach (var player in topPlayers) { var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == player.SynergyId); result.Add(new { rank = topPlayers.IndexOf(player) + 1, synergy_id = player.SynergyId, player_name = user?.SynergyId ?? "Unknown", total_records = player.RecordCount, average_time = player.AverageTime, formatted_average = FormatTime(player.AverageTime) }); } return Ok(new SynergyResponse { resultCode = 0, data = new { metric = "total_records", top_players = result } }); } else { // Rank by best average time across all records var topPlayers = await _context.PersonalRecords .GroupBy(pr => new { pr.SynergyId }) .Select(g => new { g.Key.SynergyId, RecordCount = g.Count(), AverageTime = g.Average(pr => pr.BestTimeSeconds) }) .Where(p => p.RecordCount >= 5) // Must have at least 5 records .OrderBy(p => p.AverageTime) .Take(100) .ToListAsync(); var result = new List(); foreach (var player in topPlayers) { var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == player.SynergyId); result.Add(new { rank = topPlayers.IndexOf(player) + 1, synergy_id = player.SynergyId, player_name = user?.SynergyId ?? "Unknown", total_records = player.RecordCount, average_time = player.AverageTime, formatted_average = FormatTime(player.AverageTime) }); } return Ok(new SynergyResponse { resultCode = 0, data = new { metric = "average_time", minimum_records = 5, top_players = result } }); } } /// /// Get all personal records for a player /// [HttpGet("player/{synergyId}/records")] public async Task GetPlayerRecords(string synergyId) { _logger.LogInformation("Getting personal records for {SynergyId}", synergyId); var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId); if (user == null) { return NotFound(new { error = "User not found" }); } var records = await _context.PersonalRecords .Where(pr => pr.SynergyId == synergyId) .ToListAsync(); var timeTrialRecords = new List(); var careerRecords = new List(); foreach (var record in records) { // Calculate global rank var betterTimes = await _context.LeaderboardEntries .Where(e => e.RecordType == record.RecordType && e.RecordCategory == record.RecordCategory && e.TimeSeconds < record.BestTimeSeconds) .CountAsync(); var dto = new PersonalRecordDto { RecordCategory = record.RecordCategory, TrackName = record.TrackName, CarName = record.CarName, BestTimeSeconds = record.BestTimeSeconds, FormattedTime = FormatTime(record.BestTimeSeconds), AchievedAt = record.AchievedAt, TotalAttempts = record.TotalAttempts, GlobalRank = betterTimes + 1, ImprovementSeconds = record.ImprovementSeconds }; if (record.RecordType == "TimeTrial") { timeTrialRecords.Add(dto); } else if (record.RecordType == "Career") { careerRecords.Add(dto); } } var response = new PersonalRecordsResponse { SynergyId = synergyId, PlayerName = user.SynergyId, TotalRecords = records.Count, TimeTrialRecords = timeTrialRecords, CareerRecords = careerRecords }; return Ok(new SynergyResponse { resultCode = 0, data = response }); } /// /// Compare records between two players /// [HttpGet("compare/{synergyId1}/{synergyId2}")] public async Task CompareRecords(string synergyId1, string synergyId2) { _logger.LogInformation("Comparing records between {Player1} and {Player2}", synergyId1, synergyId2); var user1 = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId1); var user2 = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId2); if (user1 == null || user2 == null) { return NotFound(new { error = "One or both users not found" }); } var records1 = await _context.PersonalRecords.Where(pr => pr.SynergyId == synergyId1).ToListAsync(); var records2 = await _context.PersonalRecords.Where(pr => pr.SynergyId == synergyId2).ToListAsync(); // Find matching records (same category) var comparisons = new List(); var allCategories = records1.Select(r => new { r.RecordType, r.RecordCategory }) .Union(records2.Select(r => new { r.RecordType, r.RecordCategory })) .Distinct() .ToList(); int player1Better = 0; int player2Better = 0; foreach (var category in allCategories) { var record1 = records1.FirstOrDefault(r => r.RecordType == category.RecordType && r.RecordCategory == category.RecordCategory); var record2 = records2.FirstOrDefault(r => r.RecordType == category.RecordType && r.RecordCategory == category.RecordCategory); string? winner = null; double? timeDiff = null; if (record1 != null && record2 != null) { if (record1.BestTimeSeconds < record2.BestTimeSeconds) { winner = "player1"; timeDiff = record2.BestTimeSeconds - record1.BestTimeSeconds; player1Better++; } else if (record2.BestTimeSeconds < record1.BestTimeSeconds) { winner = "player2"; timeDiff = record1.BestTimeSeconds - record2.BestTimeSeconds; player2Better++; } else { winner = "tie"; timeDiff = 0; } } else if (record1 != null) { winner = "player1"; player1Better++; } else if (record2 != null) { winner = "player2"; player2Better++; } comparisons.Add(new RecordComparison { RecordCategory = category.RecordCategory, TrackName = record1?.TrackName ?? record2?.TrackName, Player1Time = record1?.BestTimeSeconds, Player2Time = record2?.BestTimeSeconds, Winner = winner, TimeDifference = timeDiff }); } var response = new RecordComparisonResponse { Player1 = new PlayerRecordSummary { SynergyId = synergyId1, PlayerName = user1.SynergyId, TotalRecords = records1.Count, BetterRecords = player1Better }, Player2 = new PlayerRecordSummary { SynergyId = synergyId2, PlayerName = user2.SynergyId, TotalRecords = records2.Count, BetterRecords = player2Better }, Comparisons = comparisons }; return Ok(new SynergyResponse { resultCode = 0, data = response }); } /// /// Admin endpoint to delete a leaderboard entry by ID (for cleaning up invalid/cheated entries) /// [HttpDelete("{id:int}")] public async Task DeleteLeaderboardEntry(int id, [FromQuery] string? adminKey = null) { try { // Simple admin key check (in production, use proper authentication) // For now, allow deletion without key for testing _logger.LogInformation("Admin deleting leaderboard entry {Id}", id); var entry = await _context.LeaderboardEntries.FindAsync(id); if (entry == null) { return NotFound(new SynergyResponse { resultCode = -404, message = "Leaderboard entry not found" }); } _context.LeaderboardEntries.Remove(entry); await _context.SaveChangesAsync(); _logger.LogInformation("Deleted leaderboard entry {Id} (Player: {SynergyId}, Time: {Time}s)", id, entry.SynergyId, entry.TimeSeconds); return Ok(new SynergyResponse { resultCode = 0, message = $"Leaderboard entry {id} deleted successfully" }); } catch (Exception ex) { _logger.LogError(ex, "Error deleting leaderboard entry {Id}", id); return StatusCode(500, new SynergyResponse { resultCode = -500, message = "Internal server error" }); } } private string FormatTime(double seconds) { var timeSpan = TimeSpan.FromSeconds(seconds); return $"{(int)timeSpan.TotalMinutes}:{timeSpan.Seconds:D2}.{timeSpan.Milliseconds:D3}"; } }