using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Shared; using Managing.Domain.Accounts; using Managing.Domain.Users; 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 PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserRepository { private readonly ICacheService _cacheService; public PostgreSqlUserRepository(ManagingDbContext context, ILogger logger, SentrySqlMonitoringService sentryMonitoringService, ICacheService cacheService) : base(context, logger, sentryMonitoringService) { _cacheService = cacheService; } public async Task GetUserByAgentNameAsync(string agentName) { return await ExecuteWithLoggingAsync(async () => { // Check cache first for frequently accessed users var cacheKey = $"user_agent_{agentName}"; var cachedUser = _cacheService.GetValue(cacheKey); if (cachedUser != null) { return cachedUser; } try { await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); // Optimized query with explicit SELECT to avoid loading unnecessary data var userEntity = await _context.Users .AsNoTracking() .Where(u => u.AgentName == agentName) .Select(u => new UserEntity { Id = u.Id, Name = u.Name, AgentName = u.AgentName, AvatarUrl = u.AvatarUrl, TelegramChannel = u.TelegramChannel, OwnerWalletAddress = u.OwnerWalletAddress }) .FirstOrDefaultAsync() .ConfigureAwait(false); if (userEntity == null) return null; var user = PostgreSqlMappers.Map(userEntity); // Cache user for 5 minutes since user data doesn't change frequently _cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(5)); return user; } finally { await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); } }, nameof(GetUserByAgentNameAsync), ("agentName", agentName)); } public async Task GetUserByIdAsync(int userId) { return await ExecuteWithLoggingAsync(async () => { // Check cache first for frequently accessed users var cacheKey = $"user_id_{userId}"; var cachedUser = _cacheService.GetValue(cacheKey); if (cachedUser != null) { return cachedUser; } try { await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); // Optimized query with explicit SELECT to avoid loading unnecessary data var userEntity = await _context.Users .AsNoTracking() .Where(u => u.Id == userId) .Select(u => new UserEntity { Id = u.Id, Name = u.Name, AgentName = u.AgentName, AvatarUrl = u.AvatarUrl, TelegramChannel = u.TelegramChannel, OwnerWalletAddress = u.OwnerWalletAddress }) .FirstOrDefaultAsync() .ConfigureAwait(false); if (userEntity == null) return null; var user = PostgreSqlMappers.Map(userEntity); // Cache user for 5 minutes since user data doesn't change frequently _cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(5)); return user; } finally { await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); } }, nameof(GetUserByIdAsync), ("userId", userId)); } public async Task GetUserByNameAsync(string name, bool fetchAccounts = false) { return await ExecuteWithLoggingAsync(async () => { try { await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); User user; if (fetchAccounts) { // Fetch user with accounts in a single query var userEntity = await _context.Users .AsNoTracking() .Include(u => u.Accounts) .Where(u => u.Name == name) .FirstOrDefaultAsync() .ConfigureAwait(false); if (userEntity == null) throw new InvalidOperationException($"User with name '{name}' not found"); user = PostgreSqlMappers.Map(userEntity); // Map accounts using the existing mapper if (userEntity.Accounts != null) { user.Accounts = userEntity.Accounts.Select(PostgreSqlMappers.Map).ToList(); } else { user.Accounts = new List(); } } else { // Optimized query with explicit SELECT to avoid loading unnecessary data var userEntity = await _context.Users .AsNoTracking() .Where(u => u.Name == name) .Select(u => new UserEntity { Id = u.Id, Name = u.Name, AgentName = u.AgentName, AvatarUrl = u.AvatarUrl, TelegramChannel = u.TelegramChannel }) .FirstOrDefaultAsync() .ConfigureAwait(false); if (userEntity == null) throw new InvalidOperationException($"User with name '{name}' not found"); user = PostgreSqlMappers.Map(userEntity); user.Accounts = new List(); // Initialize empty list } return user; } finally { await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); } }, nameof(GetUserByNameAsync), ("name", name), ("fetchAccounts", fetchAccounts)); } public async Task> GetAllUsersAsync() { return await ExecuteWithLoggingAsync(async () => { // Check cache first for all users var cacheKey = "all_users"; var cachedUsers = _cacheService.GetValue>(cacheKey); if (cachedUsers != null) { return cachedUsers; } try { await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); // Optimized query with explicit SELECT to avoid loading unnecessary data var userEntities = await _context.Users .AsNoTracking() .Select(u => new UserEntity { Id = u.Id, Name = u.Name, AgentName = u.AgentName, AvatarUrl = u.AvatarUrl, TelegramChannel = u.TelegramChannel, OwnerWalletAddress = u.OwnerWalletAddress }) .ToListAsync() .ConfigureAwait(false); var users = userEntities.Select(PostgreSqlMappers.Map).ToList(); // Cache all users for 10 minutes since this data changes infrequently _cacheService.SaveValue(cacheKey, users, TimeSpan.FromMinutes(10)); return users; } finally { // Always ensure the connection is closed after the operation await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); } }, nameof(GetAllUsersAsync)); } public async Task SaveOrUpdateUserAsync(User user) { await ExecuteWithLoggingAsync(async () => { try { var existingUser = await _context.Users .AsTracking() .FirstOrDefaultAsync(u => u.Name == user.Name) .ConfigureAwait(false); string? oldAgentName = null; if (existingUser != null) { // Capture old AgentName before updating for cache invalidation oldAgentName = existingUser.AgentName; // Update existing user existingUser.AgentName = user.AgentName; existingUser.AvatarUrl = user.AvatarUrl; existingUser.TelegramChannel = user.TelegramChannel; existingUser.OwnerWalletAddress = user.OwnerWalletAddress; existingUser.IsAdmin = user.IsAdmin; _context.Users.Update(existingUser); // Update the user object with the existing user's ID user.Id = existingUser.Id; } else { // Insert new user var userEntity = PostgreSqlMappers.Map(user); _context.Users.Add(userEntity); // Update the user object with the database-generated ID after save await _context.SaveChangesAsync().ConfigureAwait(false); user.Id = userEntity.Id; // Cache the new user var newUserNameCacheKey = $"user_name_{user.Name}"; var newUserAgentCacheKey = $"user_agent_{user.AgentName}"; _cacheService.SaveValue(newUserNameCacheKey, user, TimeSpan.FromMinutes(5)); if (!string.IsNullOrEmpty(user.AgentName)) { _cacheService.SaveValue(newUserAgentCacheKey, user, TimeSpan.FromMinutes(5)); } // Invalidate all users cache since we added a new user _cacheService.RemoveValue("all_users"); return; // Exit early since we already saved } await _context.SaveChangesAsync().ConfigureAwait(false); // Invalidate cache for updated user - handle both old and new AgentName var nameCacheKey = $"user_name_{user.Name}"; var nameWithAccountsCacheKey = $"user_name_with_accounts_{user.Name}"; var idCacheKey = $"user_id_{user.Id}"; _cacheService.RemoveValue(nameCacheKey); _cacheService.RemoveValue(nameWithAccountsCacheKey); _cacheService.RemoveValue(idCacheKey); // Invalidate old AgentName cache if it existed if (!string.IsNullOrEmpty(oldAgentName)) { var oldAgentCacheKey = $"user_agent_{oldAgentName}"; _cacheService.RemoveValue(oldAgentCacheKey); } // Invalidate new AgentName cache if it exists if (!string.IsNullOrEmpty(user.AgentName)) { var newAgentCacheKey = $"user_agent_{user.AgentName}"; _cacheService.RemoveValue(newAgentCacheKey); } // Invalidate all users cache since we updated a user _cacheService.RemoveValue("all_users"); } catch (Exception e) { Console.WriteLine(e); throw new Exception("Cannot save or update user"); } }, nameof(SaveOrUpdateUserAsync), ("userName", user.Name), ("userId", user.Id)); } public async Task<(IEnumerable Users, int TotalCount)> GetUsersPaginatedAsync(int page, int pageSize, UserSortableColumn sortBy, string sortOrder, UsersFilter filter) { return await ExecuteWithLoggingAsync(async () => { try { await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); var query = _context.Users.AsNoTracking(); // Apply filters if (!string.IsNullOrWhiteSpace(filter.UserNameContains)) { query = query.Where(u => EF.Functions.ILike(u.Name, $"%{filter.UserNameContains.Trim()}%")); } if (!string.IsNullOrWhiteSpace(filter.OwnerAddressContains)) { query = query.Where(u => EF.Functions.ILike(u.OwnerWalletAddress, $"%{filter.OwnerAddressContains.Trim()}%")); } if (!string.IsNullOrWhiteSpace(filter.AgentNameContains)) { query = query.Where(u => EF.Functions.ILike(u.AgentName, $"%{filter.AgentNameContains.Trim()}%")); } if (!string.IsNullOrWhiteSpace(filter.TelegramChannelContains)) { query = query.Where(u => EF.Functions.ILike(u.TelegramChannel, $"%{filter.TelegramChannelContains.Trim()}%")); } // Get total count for pagination var totalCount = await query.CountAsync().ConfigureAwait(false); // Apply sorting query = sortBy switch { UserSortableColumn.Id => sortOrder.ToLower() == "desc" ? query.OrderByDescending(u => u.Id) : query.OrderBy(u => u.Id), UserSortableColumn.Name => sortOrder.ToLower() == "desc" ? query.OrderByDescending(u => u.Name) : query.OrderBy(u => u.Name), UserSortableColumn.OwnerWalletAddress => sortOrder.ToLower() == "desc" ? query.OrderByDescending(u => u.OwnerWalletAddress) : query.OrderBy(u => u.OwnerWalletAddress), UserSortableColumn.AgentName => sortOrder.ToLower() == "desc" ? query.OrderByDescending(u => u.AgentName) : query.OrderBy(u => u.AgentName), _ => query.OrderBy(u => u.Id) // Default sorting }; // Apply pagination var users = await query .Skip((page - 1) * pageSize) .Take(pageSize) .Select(u => new UserEntity { Id = u.Id, Name = u.Name, AgentName = u.AgentName, AvatarUrl = u.AvatarUrl, TelegramChannel = u.TelegramChannel, OwnerWalletAddress = u.OwnerWalletAddress, IsAdmin = u.IsAdmin, LastConnectionDate = u.LastConnectionDate }) .ToListAsync() .ConfigureAwait(false); var domainUsers = users.Select(PostgreSqlMappers.Map).ToList(); return (domainUsers, totalCount); } finally { await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); } }, nameof(GetUsersPaginatedAsync), ("page", page), ("pageSize", pageSize), ("sortBy", sortBy), ("sortOrder", sortOrder)); } }