using Managing.Application.Abstractions.Repositories; using Managing.Domain.Statistics; using Managing.Infrastructure.Databases.PostgreSql.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; namespace Managing.Infrastructure.Databases.PostgreSql; public class AgentSummaryRepository : IAgentSummaryRepository { private readonly ManagingDbContext _context; private readonly ILogger _logger; public AgentSummaryRepository(ManagingDbContext context, ILogger logger) { _context = context; _logger = logger; } public async Task GetByUserIdAsync(int userId) { var entity = await _context.AgentSummaries .Include(a => a.User) .FirstOrDefaultAsync(a => a.UserId == userId); return entity != null ? MapToDomain(entity) : null; } public async Task GetByAgentNameAsync(string agentName) { var entity = await _context.AgentSummaries .Include(a => a.User) .FirstOrDefaultAsync(a => a.AgentName == agentName); return entity != null ? MapToDomain(entity) : null; } public async Task> GetAllAsync() { var entities = await _context.AgentSummaries .Include(a => a.User) .ToListAsync(); return entities.Select(MapToDomain); } public async Task InsertAsync(AgentSummary agentSummary) { var entity = MapToEntity(agentSummary); entity.CreatedAt = DateTime.UtcNow; entity.UpdatedAt = DateTime.UtcNow; await _context.AgentSummaries.AddAsync(entity); await _context.SaveChangesAsync(); _logger.LogInformation("AgentSummary inserted for user {UserId} with agent name {AgentName}", agentSummary.UserId, agentSummary.AgentName); } public async Task UpdateAsync(AgentSummary agentSummary) { var entity = await _context.AgentSummaries .FirstOrDefaultAsync(a => a.UserId == agentSummary.UserId); if (entity != null) { MapToEntity(agentSummary, entity); entity.UpdatedAt = DateTime.UtcNow; // No need to call Update() since the entity is already being tracked await _context.SaveChangesAsync(); _logger.LogInformation("AgentSummary updated for user {UserId} with agent name {AgentName}", agentSummary.UserId, agentSummary.AgentName); } } public async Task SaveOrUpdateAsync(AgentSummary agentSummary) { // Use the execution strategy to handle retries and transactions var strategy = _context.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => { using var transaction = await _context.Database.BeginTransactionAsync(); try { // Ensure the User entity exists and is saved if (agentSummary.User != null) { var existingUser = await _context.Users .AsNoTracking() .FirstOrDefaultAsync(u => u.Id == agentSummary.UserId); if (existingUser == null) { // User doesn't exist, save it first var userEntity = PostgreSqlMappers.Map(agentSummary.User); userEntity.Id = agentSummary.UserId; // Ensure the ID is set await _context.Users.AddAsync(userEntity); await _context.SaveChangesAsync(); _logger.LogInformation("User created for AgentSummary with ID {UserId}", agentSummary.UserId); } } // Check if AgentSummary exists (using AsNoTracking to avoid tracking conflicts) var existing = await _context.AgentSummaries .AsNoTracking() .FirstOrDefaultAsync(a => a.UserId == agentSummary.UserId); if (existing == null) { // Insert new AgentSummary var entity = MapToEntity(agentSummary); entity.CreatedAt = DateTime.UtcNow; entity.UpdatedAt = DateTime.UtcNow; await _context.AgentSummaries.AddAsync(entity); await _context.SaveChangesAsync(); _logger.LogInformation("AgentSummary inserted for user {UserId} with agent name {AgentName}", agentSummary.UserId, agentSummary.AgentName); } else { // Update existing record - check if already tracked first var entityToUpdate = MapToEntity(agentSummary); entityToUpdate.Id = existing.Id; // Preserve the existing ID entityToUpdate.CreatedAt = existing.CreatedAt; // Preserve creation date entityToUpdate.UpdatedAt = DateTime.UtcNow; // Check if an entity with this key is already being tracked var trackedEntity = _context.ChangeTracker.Entries() .FirstOrDefault(e => e.Entity.Id == existing.Id); if (trackedEntity != null) { // Entity is already tracked, update its values MapToEntity(agentSummary, trackedEntity.Entity); trackedEntity.Entity.UpdatedAt = DateTime.UtcNow; trackedEntity.State = EntityState.Modified; } else { // Entity is not tracked, use Update method which handles state properly _context.AgentSummaries.Update(entityToUpdate); } await _context.SaveChangesAsync(); _logger.LogInformation("AgentSummary updated for user {UserId} with agent name {AgentName}", agentSummary.UserId, agentSummary.AgentName); } await transaction.CommitAsync(); } catch (Exception ex) { await transaction.RollbackAsync(); _logger.LogError(ex, "Error saving/updating AgentSummary for user {UserId} with agent name {AgentName}", agentSummary.UserId, agentSummary.AgentName); throw; } }); } public async Task<(IEnumerable Results, int TotalCount)> GetPaginatedAsync( int page, int pageSize, SortableFields sortBy, string sortOrder, IEnumerable? agentNames = null, bool showOnlyProfitableAgent = false) { // Start with base query var query = _context.AgentSummaries.Include(a => a.User).AsQueryable(); // Apply agent name filtering if specified if (agentNames != null && agentNames.Any()) { query = query.Where(a => agentNames.Contains(a.AgentName)); } // Apply profitable agent filtering if specified if (showOnlyProfitableAgent) { query = query.Where(a => a.TotalROI > 0); } // Get total count before applying pagination var totalCount = await query.CountAsync(); // Apply sorting var isDescending = sortOrder.ToLowerInvariant() == "desc"; query = sortBy switch { SortableFields.NetPnL => isDescending ? query.OrderByDescending(a => a.NetPnL) : query.OrderBy(a => a.NetPnL), SortableFields.TotalROI => isDescending ? query.OrderByDescending(a => a.TotalROI) : query.OrderBy(a => a.TotalROI), SortableFields.Wins => isDescending ? query.OrderByDescending(a => a.Wins) : query.OrderBy(a => a.Wins), SortableFields.Losses => isDescending ? query.OrderByDescending(a => a.Losses) : query.OrderBy(a => a.Losses), SortableFields.AgentName => isDescending ? query.OrderByDescending(a => a.AgentName) : query.OrderBy(a => a.AgentName), SortableFields.CreatedAt => isDescending ? query.OrderByDescending(a => a.CreatedAt) : query.OrderBy(a => a.CreatedAt), SortableFields.UpdatedAt => isDescending ? query.OrderByDescending(a => a.UpdatedAt) : query.OrderBy(a => a.UpdatedAt), SortableFields.TotalVolume => isDescending ? query.OrderByDescending(a => a.TotalVolume) : query.OrderBy(a => a.TotalVolume), SortableFields.TotalBalance => isDescending ? query.OrderByDescending(a => a.TotalBalance) : query.OrderBy(a => a.TotalBalance), _ => isDescending ? query.OrderByDescending(a => a.TotalPnL) // Default to TotalPnL desc : query.OrderBy(a => a.TotalPnL) }; // Apply pagination var results = await query .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); // Map to domain objects var domainResults = results.Select(MapToDomain); return (domainResults, totalCount); } private static AgentSummaryEntity MapToEntity(AgentSummary domain) { return new AgentSummaryEntity { Id = domain.Id, UserId = domain.UserId, AgentName = domain.AgentName, TotalPnL = domain.TotalPnL, NetPnL = domain.NetPnL, TotalROI = domain.TotalROI, Wins = domain.Wins, Losses = domain.Losses, Runtime = domain.Runtime, CreatedAt = domain.CreatedAt, UpdatedAt = domain.UpdatedAt, ActiveStrategiesCount = domain.ActiveStrategiesCount, TotalVolume = domain.TotalVolume, TotalBalance = domain.TotalBalance, TotalFees = domain.TotalFees, BacktestCount = domain.BacktestCount }; } private static void MapToEntity(AgentSummary domain, AgentSummaryEntity entity) { entity.UserId = domain.UserId; entity.AgentName = domain.AgentName; entity.TotalPnL = domain.TotalPnL; entity.NetPnL = domain.NetPnL; entity.TotalROI = domain.TotalROI; entity.Wins = domain.Wins; entity.Losses = domain.Losses; entity.Runtime = domain.Runtime; entity.ActiveStrategiesCount = domain.ActiveStrategiesCount; entity.TotalVolume = domain.TotalVolume; entity.TotalBalance = domain.TotalBalance; entity.TotalFees = domain.TotalFees; // BacktestCount is NOT updated here - it's managed independently via IncrementBacktestCountAsync // This prevents other update operations from overwriting the BacktestCount } private static AgentSummary MapToDomain(AgentSummaryEntity entity) { return new AgentSummary { Id = entity.Id, UserId = entity.UserId, AgentName = entity.AgentName, TotalPnL = entity.TotalPnL, NetPnL = entity.NetPnL, TotalROI = entity.TotalROI, Wins = entity.Wins, Losses = entity.Losses, Runtime = entity.Runtime, CreatedAt = entity.CreatedAt, UpdatedAt = entity.UpdatedAt, ActiveStrategiesCount = entity.ActiveStrategiesCount, TotalVolume = entity.TotalVolume, TotalBalance = entity.TotalBalance, TotalFees = entity.TotalFees, BacktestCount = entity.BacktestCount, User = PostgreSqlMappers.Map(entity.User) }; } public async Task> GetAllAgentWithRunningBots() { var agentSummaries = await _context.AgentSummaries .Include(a => a.User) .Where(a => _context.Bots.Any(b => b.UserId == a.UserId && b.Status == BotStatus.Running)) .ToListAsync(); return agentSummaries.Select(MapToDomain); } public async Task UpdateAgentNameAsync(int userId, string agentName) { try { // First, check if there's already a tracked entity with this key var trackedEntity = _context.ChangeTracker.Entries() .FirstOrDefault(e => e.Entity.UserId == userId); AgentSummaryEntity? entityToUpdate = null; bool wasTracked = false; if (trackedEntity != null) { // Entity is already tracked, update it directly entityToUpdate = trackedEntity.Entity; wasTracked = true; _logger.LogInformation("Found tracked entity for user {UserId}. Current agent name: {CurrentAgentName}, New agent name: {NewAgentName}", userId, entityToUpdate.AgentName, agentName); } else { // Entity is not tracked, fetch it normally entityToUpdate = await _context.AgentSummaries .FirstOrDefaultAsync(a => a.UserId == userId); if (entityToUpdate == null) { _logger.LogWarning( "No AgentSummary found for user {UserId} when trying to update agent name to {AgentName}", userId, agentName); return; } _logger.LogInformation("Fetched entity for user {UserId}. Current agent name: {CurrentAgentName}, New agent name: {NewAgentName}", userId, entityToUpdate.AgentName, agentName); } // Check if the agent name is actually different if (entityToUpdate.AgentName == agentName) { _logger.LogInformation("Agent name for user {UserId} is already {AgentName}, no update needed", userId, agentName); return; } // Update the entity var oldAgentName = entityToUpdate.AgentName; entityToUpdate.AgentName = agentName; entityToUpdate.UpdatedAt = DateTime.UtcNow; // If it wasn't tracked, explicitly mark it as modified if (!wasTracked) { _context.Entry(entityToUpdate).State = EntityState.Modified; } // Log the change tracker state before saving var modifiedEntries = _context.ChangeTracker.Entries() .Where(e => e.State == EntityState.Modified) .ToList(); _logger.LogInformation("Change tracker has {Count} modified entries before save", modifiedEntries.Count); // Save changes var changesSaved = await _context.SaveChangesAsync(); _logger.LogInformation("Agent name updated for user {UserId} from '{OldAgentName}' to '{NewAgentName}'. Changes saved: {ChangesSaved}", userId, oldAgentName, agentName, changesSaved); if (changesSaved == 0) { _logger.LogWarning("No changes were saved for user {UserId}. This might indicate a tracking issue.", userId); } } catch (Exception ex) { _logger.LogError(ex, "Error updating agent name for user {UserId} to {AgentName}", userId, agentName); throw; } } public async Task GetTotalAgentCount() { return await _context.AgentSummaries.CountAsync(); } public async Task IncrementBacktestCountAsync(int userId) { try { // First, check if there's already a tracked entity with this key var trackedEntity = _context.ChangeTracker.Entries() .FirstOrDefault(e => e.Entity.UserId == userId); AgentSummaryEntity? entityToUpdate = null; bool wasTracked = false; if (trackedEntity != null) { // Entity is already tracked, update it directly entityToUpdate = trackedEntity.Entity; wasTracked = true; _logger.LogInformation("Found tracked entity for user {UserId}. Current backtest count: {CurrentCount}", userId, entityToUpdate.BacktestCount); } else { // Entity is not tracked, fetch it normally entityToUpdate = await _context.AgentSummaries .FirstOrDefaultAsync(a => a.UserId == userId); if (entityToUpdate == null) { _logger.LogWarning("No AgentSummary found for user {UserId} when trying to increment backtest count", userId); return; } _logger.LogInformation("Fetched entity for user {UserId}. Current backtest count: {CurrentCount}", userId, entityToUpdate.BacktestCount); } // Update the entity var oldCount = entityToUpdate.BacktestCount; var newCount = oldCount + 1; entityToUpdate.BacktestCount = newCount; entityToUpdate.UpdatedAt = DateTime.UtcNow; // If it wasn't tracked, explicitly mark it as modified if (!wasTracked) { _context.Entry(entityToUpdate).State = EntityState.Modified; } // Log the change tracker state before saving var modifiedEntries = _context.ChangeTracker.Entries() .Where(e => e.State == EntityState.Modified) .ToList(); _logger.LogInformation("Change tracker has {Count} modified entries before save", modifiedEntries.Count); // Save changes var changesSaved = await _context.SaveChangesAsync(); _logger.LogInformation("Backtest count incremented for user {UserId} from {OldCount} to {NewCount}. Changes saved: {ChangesSaved}", userId, oldCount, newCount, changesSaved); if (changesSaved == 0) { _logger.LogWarning("No changes were saved for user {UserId}. This might indicate a tracking issue.", userId); } } catch (Exception ex) { _logger.LogError(ex, "Error incrementing backtest count for user {UserId}", userId); throw; } } public async Task UpdateTotalBalanceAsync(int userId, decimal totalBalance) { try { // First, check if there's already a tracked entity with this key var trackedEntity = _context.ChangeTracker.Entries() .FirstOrDefault(e => e.Entity.UserId == userId); AgentSummaryEntity? entityToUpdate = null; bool wasTracked = false; if (trackedEntity != null) { // Entity is already tracked, update it directly entityToUpdate = trackedEntity.Entity; wasTracked = true; _logger.LogInformation("Found tracked entity for user {UserId}. Current total balance: {CurrentBalance}, New total balance: {NewBalance}", userId, entityToUpdate.TotalBalance, totalBalance); } else { // Entity is not tracked, fetch it normally entityToUpdate = await _context.AgentSummaries .FirstOrDefaultAsync(a => a.UserId == userId); if (entityToUpdate == null) { _logger.LogWarning("No AgentSummary found for user {UserId} when trying to update total balance to {TotalBalance}", userId, totalBalance); return; } _logger.LogInformation("Fetched entity for user {UserId}. Current total balance: {CurrentBalance}, New total balance: {NewBalance}", userId, entityToUpdate.TotalBalance, totalBalance); } // Check if the total balance is actually different if (entityToUpdate.TotalBalance == totalBalance) { _logger.LogInformation("Total balance for user {UserId} is already {TotalBalance}, no update needed", userId, totalBalance); return; } // Update the entity var oldBalance = entityToUpdate.TotalBalance; entityToUpdate.TotalBalance = totalBalance; entityToUpdate.UpdatedAt = DateTime.UtcNow; // If it wasn't tracked, explicitly mark it as modified if (!wasTracked) { _context.Entry(entityToUpdate).State = EntityState.Modified; } // Log the change tracker state before saving var modifiedEntries = _context.ChangeTracker.Entries() .Where(e => e.State == EntityState.Modified) .ToList(); _logger.LogInformation("Change tracker has {Count} modified entries before save", modifiedEntries.Count); // Save changes var changesSaved = await _context.SaveChangesAsync(); _logger.LogInformation("Total balance updated for user {UserId} from {OldBalance} to {NewBalance}. Changes saved: {ChangesSaved}", userId, oldBalance, totalBalance, changesSaved); if (changesSaved == 0) { _logger.LogWarning("No changes were saved for user {UserId}. This might indicate a tracking issue.", userId); } } catch (Exception ex) { _logger.LogError(ex, "Error updating total balance for user {UserId} to {TotalBalance}", userId, totalBalance); throw; } } }