using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MotoresArgentinosV2.Infrastructure.Data; using MotoresArgentinosV2.Core.Entities; using MotoresArgentinosV2.Core.Interfaces; using Microsoft.AspNetCore.Authorization; using MotoresArgentinosV2.Core.DTOs; namespace MotoresArgentinosV2.API.Controllers; [Authorize(Roles = "Admin")] [ApiController] [Route("api/[controller]")] public class AdminController : ControllerBase { private readonly MotoresV2DbContext _context; private readonly IAdSyncService _syncService; private readonly INotificationService _notificationService; public AdminController(MotoresV2DbContext context, IAdSyncService syncService, INotificationService notificationService) { _context = context; _syncService = syncService; _notificationService = notificationService; } // --- MODERACIÓN --- // GESTIÓN GLOBAL DE AVISOS (Buscador Admin) [HttpGet("ads")] public async Task GetAllAds( [FromQuery] string? q = null, [FromQuery] int? statusId = null, [FromQuery] int page = 1) { const int pageSize = 20; var query = _context.Ads .Include(a => a.Brand) .Include(a => a.User) .Include(a => a.Photos) // Para mostrar la miniatura .AsNoTracking() // Optimización de lectura .AsQueryable(); // Filtro por Texto (Marca, Modelo, Email Usuario, Nombre Usuario) if (!string.IsNullOrEmpty(q)) { query = query.Where(a => a.VersionName!.Contains(q) || a.Brand.Name.Contains(q) || a.User.Email.Contains(q) || a.User.UserName.Contains(q) ); } // Filtro por Estado if (statusId.HasValue) { query = query.Where(a => a.StatusID == statusId.Value); } var total = await query.CountAsync(); var ads = await query .OrderByDescending(a => a.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(a => new { a.AdID, BrandName = a.Brand != null ? a.Brand.Name : null, VersionName = a.VersionName, StatusID = a.StatusID, CreatedAt = a.CreatedAt, PublishedAt = a.PublishedAt, ExpiresAt = a.ExpiresAt, DeletedAt = a.DeletedAt, Views = a.ViewsCounter, LegacyID = a.LegacyAdID, // Datos de la transacción APROBADA más reciente PaidAmount = _context.Transactions .Where(t => t.AdID == a.AdID && t.Status == "APPROVED") .OrderByDescending(t => t.CreatedAt) .Select(t => (decimal?)t.Amount) // Cast a nullable para detectar si no hubo pago .FirstOrDefault(), PaidDate = _context.Transactions .Where(t => t.AdID == a.AdID && t.Status == "APPROVED") .OrderByDescending(t => t.CreatedAt) .Select(t => (DateTime?)t.CreatedAt) .FirstOrDefault(), UserEmail = a.User.Email, UserName = a.User.UserName, Thumbnail = a.Photos.Where(p => p.IsCover).Select(p => p.FilePath).FirstOrDefault() ?? a.Photos.Select(p => p.FilePath).FirstOrDefault() }) .ToListAsync(); return Ok(new { ads, total, page, pageSize }); } [HttpGet("ads/pending")] public async Task GetPendingAds() { var ads = await _context.Ads .Include(a => a.User) .Include(a => a.Photos) .Where(a => a.StatusID == (int)AdStatusEnum.ModerationPending) .OrderByDescending(a => a.CreatedAt) .Select(a => new { a.AdID, VersionName = a.Brand != null ? $"{a.Brand.Name} {a.VersionName}" : a.VersionName, a.Price, a.Currency, a.CreatedAt, UserID = a.UserID, UserName = a.User.UserName, Email = a.User.Email, Thumbnail = a.Photos.Where(p => p.IsCover).Select(p => p.FilePath).FirstOrDefault() ?? a.Photos.Select(p => p.FilePath).FirstOrDefault() }) .ToListAsync(); return Ok(ads); } [HttpPost("ads/{id}/approve")] public async Task ApproveAd(int id) { var ad = await _context.Ads.Include(a => a.User).Include(a => a.Brand).FirstOrDefaultAsync(a => a.AdID == id); if (ad == null) return NotFound(); ad.StatusID = (int)AdStatusEnum.Active; ad.PublishedAt = DateTime.UtcNow; ad.ExpiresAt = DateTime.UtcNow.AddDays(30); await _context.SaveChangesAsync(); // Audit Log var adminId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); _context.AuditLogs.Add(new AuditLog { Action = "AD_APPROVED", Entity = "Ad", EntityID = id, UserID = adminId, Details = $"Aviso '{ad.Brand?.Name} {ad.VersionName}' aprobado por administrador." }); await _context.SaveChangesAsync(); // Sincronizar a Legacy try { await _syncService.SyncAdToLegacyAsync(id); var adTitle = $"{ad.Brand?.Name} {ad.VersionName}"; await _notificationService.SendAdStatusChangedEmailAsync(ad.User?.Email ?? string.Empty, adTitle, "APROBADO"); } catch (Exception) { // Logueamos pero no bloqueamos la aprobación en V2 } return Ok(new { message = "Aviso aprobado y publicado." }); } [HttpPost("ads/{id}/reject")] public async Task RejectAd(int id, [FromBody] string reason) { var ad = await _context.Ads.Include(a => a.User).Include(a => a.Brand).FirstOrDefaultAsync(a => a.AdID == id); if (ad == null) return NotFound(); ad.StatusID = (int)AdStatusEnum.Rejected; await _context.SaveChangesAsync(); // Audit Log var adminId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); _context.AuditLogs.Add(new AuditLog { Action = "AD_REJECTED", Entity = "Ad", EntityID = id, UserID = adminId, Details = $"Aviso '{ad.Brand?.Name} {ad.VersionName}' rechazado. Motivo: {reason}" }); await _context.SaveChangesAsync(); // Notificar rechazo var adTitle = $"{ad.Brand?.Name} {ad.VersionName}"; await _notificationService.SendAdStatusChangedEmailAsync(ad.User?.Email ?? string.Empty, adTitle, "RECHAZADO", reason); return Ok(new { message = "Aviso rechazado." }); } // --- TRANSACCIONES --- [HttpGet("transactions")] public async Task GetTransactions( [FromQuery] string? status = null, [FromQuery] string? userSearch = null, [FromQuery] DateTime? fromDate = null, [FromQuery] DateTime? toDate = null, [FromQuery] int page = 1) { const int pageSize = 20; var query = from t in _context.Transactions join a in _context.Ads on t.AdID equals a.AdID into adGroup from ad in adGroup.DefaultIfEmpty() // Left Join con Ads join u in _context.Users on ad.UserID equals u.UserID into userGroup from user in userGroup.DefaultIfEmpty() // Left Join con Users select new { t, ad, user }; // 1. Filtro por Estado if (!string.IsNullOrEmpty(status)) { query = query.Where(x => x.t.Status == status); } // 2. Filtro por Usuario (Búsqueda) if (!string.IsNullOrEmpty(userSearch)) { // Solo buscamos si el usuario existe (no es nulo) query = query.Where(x => x.user != null && (x.user.Email.Contains(userSearch) || x.user.UserName.Contains(userSearch)) ); } // 3. Filtro por Fechas if (fromDate.HasValue) { query = query.Where(x => x.t.CreatedAt >= fromDate.Value); } if (toDate.HasValue) { var toDateEndOfDay = toDate.Value.Date.AddHours(23).AddMinutes(59).AddSeconds(59); query = query.Where(x => x.t.CreatedAt <= toDateEndOfDay); } // 4. Paginación y Ejecución var total = await query.CountAsync(); var txs = await query .OrderByDescending(x => x.t.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(x => new { x.t.TransactionID, x.t.OperationCode, x.t.Amount, x.t.Status, x.t.CreatedAt, x.t.UpdatedAt, AdID = x.t.AdID, // LÓGICA DE PRIORIDAD: Snapshot > Relación Viva > Default UserID = (x.ad != null) ? x.ad.UserID : 0, UserName = !string.IsNullOrEmpty(x.t.SnapshotUserName) ? x.t.SnapshotUserName : ((x.user != null) ? x.user.UserName : "ELIMINADO"), UserEmail = !string.IsNullOrEmpty(x.t.SnapshotUserEmail) ? x.t.SnapshotUserEmail : ((x.user != null) ? x.user.Email : "-"), AdTitle = !string.IsNullOrEmpty(x.t.SnapshotAdTitle) ? x.t.SnapshotAdTitle : ((x.ad != null && x.ad.Brand != null) ? $"{x.ad.Brand.Name} {x.ad.VersionName}" : "Vehículo Desconocido/Eliminado") }) .ToListAsync(); return Ok(new { transactions = txs, total, page, pageSize }); } // --- USUARIOS --- [HttpGet("users")] public async Task GetUsers([FromQuery] string? q = null, [FromQuery] int page = 1) { const int pageSize = 20; var query = _context.Users.AsQueryable(); if (!string.IsNullOrEmpty(q)) { query = query.Where(u => (u.Email ?? "").Contains(q) || (u.UserName ?? "").Contains(q) || (u.FirstName ?? "").Contains(q) || (u.LastName ?? "").Contains(q)); } var total = await query.CountAsync(); var users = await query .OrderByDescending(u => u.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(u => new { u.UserID, u.UserName, u.Email, u.FirstName, u.LastName, u.UserType, u.MigrationStatus, u.IsBlocked, u.CreatedAt }) .ToListAsync(); return Ok(new { users, total, page, pageSize }); } [HttpGet("users/{id}")] public async Task GetUserDetail(int id) { var user = await _context.Users .Where(u => u.UserID == id) .Select(u => new UserDetailDto { UserID = u.UserID, UserName = u.UserName, Email = u.Email, FirstName = u.FirstName, LastName = u.LastName, PhoneNumber = u.PhoneNumber, UserType = u.UserType, IsBlocked = u.IsBlocked, MigrationStatus = u.MigrationStatus, IsEmailVerified = u.IsEmailVerified, CreatedAt = u.CreatedAt }) .FirstOrDefaultAsync(); if (user == null) return NotFound(); return Ok(user); } [HttpPut("users/{id}")] public async Task UpdateUser(int id, [FromBody] UserUpdateDto dto) { var user = await _context.Users.FindAsync(id); if (user == null) return NotFound(); user.UserName = dto.UserName; user.FirstName = dto.FirstName; user.LastName = dto.LastName; user.PhoneNumber = dto.PhoneNumber; user.UserType = dto.UserType; await _context.SaveChangesAsync(); // Audit Log var adminId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); _context.AuditLogs.Add(new AuditLog { Action = "USER_UPDATED", Entity = "User", EntityID = id, UserID = adminId, Details = $"Usuario '{user.UserName}' actualizado por administrador." }); await _context.SaveChangesAsync(); return Ok(new { message = "Usuario actualizado correctamente." }); } [HttpGet("stats")] public async Task GetStats() { var stats = new { // 1. Usamos AuditLogs para contar TODAS las creaciones históricas, // incluso si el aviso ya fue borrado físicamente de la tabla Ads. TotalAds = await _context.AuditLogs.CountAsync(l => l.Action == "AD_CREATED"), // 2. Avisos actualmente activos (Status = 4) ActiveAds = await _context.Ads.CountAsync(a => a.StatusID == (int)AdStatusEnum.Active), // 3. Pendientes de moderación (Status = 3) PendingAds = await _context.Ads.CountAsync(a => a.StatusID == (int)AdStatusEnum.ModerationPending), // 4. Total de Usuarios TotalUsers = await _context.Users.CountAsync(), // 5. Mensajes NO LEÍDOS (IsRead = false) UnreadMessages = await _context.ChatMessages.CountAsync(m => !m.IsRead) }; return Ok(stats); } [HttpGet("audit")] public async Task GetAuditLogs( [FromQuery] string? actionType = null, [FromQuery] string? entity = null, [FromQuery] int? userId = null, [FromQuery] DateTime? fromDate = null, [FromQuery] DateTime? toDate = null, [FromQuery] int page = 1) { const int pageSize = 50; var query = _context.AuditLogs.AsQueryable(); if (!string.IsNullOrEmpty(actionType)) query = query.Where(l => l.Action == actionType); if (!string.IsNullOrEmpty(entity)) query = query.Where(l => l.Entity == entity); if (userId.HasValue) query = query.Where(l => l.UserID == userId.Value); if (fromDate.HasValue) query = query.Where(l => l.CreatedAt >= fromDate.Value); if (toDate.HasValue) query = query.Where(l => l.CreatedAt <= toDate.Value); var total = await query.CountAsync(); var logs = await query .OrderByDescending(l => l.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(l => new { l.AuditLogID, l.Action, l.Entity, l.EntityID, l.UserID, l.Details, l.CreatedAt, UserName = l.UserID == 0 ? "Sistema" : _context.Users.Where(u => u.UserID == l.UserID).Select(u => u.UserName).FirstOrDefault() ?? "Desconocido" }) .ToListAsync(); return Ok(new { logs, total, page, pageSize }); } // BÚSQUEDA DE USUARIOS PARA ASIGNAR AVISO [HttpGet("users/search")] public async Task SearchUsers([FromQuery] string q) { if (string.IsNullOrEmpty(q)) return Ok(new List()); var users = await _context.Users .Where(u => (u.Email ?? "").Contains(q) || (u.UserName ?? "").Contains(q) || (u.FirstName ?? "").Contains(q) || (u.LastName ?? "").Contains(q)) .Take(10) .Select(u => new { u.UserID, u.UserName, u.Email, u.FirstName, u.LastName, u.PhoneNumber, u.IsBlocked }) .ToListAsync(); return Ok(users); } // GESTIÓN DE BLOQUEO [HttpPost("users/{id}/toggle-block")] public async Task ToggleBlockUser(int id) { var user = await _context.Users.FindAsync(id); if (user == null) return NotFound(); // Evitar autobloqueo var currentAdminId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); if (user.UserID == currentAdminId) return BadRequest("No puedes bloquearte a ti mismo."); user.IsBlocked = !user.IsBlocked; // 📝 AUDITORÍA _context.AuditLogs.Add(new AuditLog { Action = user.IsBlocked ? "USER_BLOCKED" : "USER_UNBLOCKED", Entity = "User", EntityID = user.UserID, UserID = currentAdminId, Details = $"Usuario '{user.UserName}' {(user.IsBlocked ? "bloqueado" : "desbloqueado")} por administrador." }); await _context.SaveChangesAsync(); return Ok(new { message = user.IsBlocked ? "Usuario bloqueado" : "Usuario desbloqueado", isBlocked = user.IsBlocked }); } // REPUBLICAR AVISO VENCIDO [HttpPost("ads/{id}/republish")] public async Task RepublishAd(int id) { var ad = await _context.Ads.Include(a => a.Brand).FirstOrDefaultAsync(a => a.AdID == id); if (ad == null) return NotFound(); // Verificación de seguridad: solo se pueden republicar avisos vencidos. if (ad.StatusID != (int)AdStatusEnum.Expired) { return BadRequest("Solo se pueden republicar avisos que se encuentren vencidos."); } // Actualizamos el estado y las fechas para un nuevo ciclo de 30 días. ad.StatusID = (int)AdStatusEnum.Active; ad.PublishedAt = DateTime.UtcNow; // La nueva fecha de publicación es ahora. ad.ExpiresAt = DateTime.UtcNow.AddDays(30); ad.ExpirationWarningSent = false; // Reseteamos la advertencia de expiración. // Audit Log var adminId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); _context.AuditLogs.Add(new AuditLog { Action = "AD_REPUBLISHED_BY_ADMIN", Entity = "Ad", EntityID = id, UserID = adminId, Details = $"Aviso ID {id} ('{ad.Brand?.Name} {ad.VersionName}') republicado por administrador." }); await _context.SaveChangesAsync(); // Aquí podrías agregar una sincronización con el sistema legacy si fuera necesario. // await _syncService.SyncAdToLegacyAsync(id); return Ok(new { message = "Aviso republicado y activado por 30 días." }); } }