using Dapper; using Inventario.API.Data; using Inventario.API.DTOs; using Inventario.API.Helpers; using Inventario.API.Models; using Microsoft.AspNetCore.Mvc; using System.Data; using System.Net.NetworkInformation; using Microsoft.Data.SqlClient; namespace Inventario.API.Controllers { [ApiController] [Route("api/[controller]")] public class EquiposController : ControllerBase { private readonly DapperContext _context; public EquiposController(DapperContext context) { _context = context; } // --- MÉTODOS CRUD BÁSICOS (Ya implementados) --- // GET /api/equipos [HttpGet] public async Task Consultar() { var query = @" SELECT e.Id, e.Hostname, e.Ip, e.Mac, e.Motherboard, e.Cpu, e.Ram_installed, e.Ram_slots, e.Os, e.Architecture, s.Id as Id, s.Nombre, u.Id as Id, u.Username, u.Password, d.Id as Id, d.Mediatype, d.Size, mr.Id as Id, mr.part_number as PartNumber, mr.Fabricante, mr.Tamano, mr.Velocidad, emr.Slot FROM dbo.equipos e LEFT JOIN dbo.sectores s ON e.sector_id = s.id LEFT JOIN dbo.usuarios_equipos ue ON e.id = ue.equipo_id LEFT JOIN dbo.usuarios u ON ue.usuario_id = u.id LEFT JOIN dbo.equipos_discos ed ON e.id = ed.equipo_id LEFT JOIN dbo.discos d ON ed.disco_id = d.id LEFT JOIN dbo.equipos_memorias_ram emr ON e.id = emr.equipo_id LEFT JOIN dbo.memorias_ram mr ON emr.memoria_ram_id = mr.id;"; using (var connection = _context.CreateConnection()) { var equipoDict = new Dictionary(); await connection.QueryAsync( query, (equipo, sector, usuario, disco, memoria) => { if (!equipoDict.TryGetValue(equipo.Id, out var equipoActual)) { equipoActual = equipo; equipoActual.Sector = sector; equipoDict.Add(equipoActual.Id, equipoActual); } if (usuario != null && !equipoActual.Usuarios.Any(u => u.Id == usuario.Id)) equipoActual.Usuarios.Add(usuario); if (disco != null && !equipoActual.Discos.Any(d => d.Id == disco.Id)) equipoActual.Discos.Add(disco); if (memoria != null && !equipoActual.MemoriasRam.Any(m => m.Id == memoria.Id && m.Slot == memoria.Slot)) equipoActual.MemoriasRam.Add(memoria); return equipoActual; }, splitOn: "Id,Id,Id,Id" // Dapper divide en cada 'Id' ); return Ok(equipoDict.Values.OrderBy(e => e.Sector?.Nombre).ThenBy(e => e.Hostname)); } } // --- GET /api/equipos/{hostname} --- [HttpGet("{hostname}")] public async Task ConsultarDetalle(string hostname) { var query = @"SELECT e.*, s.Id as SectorId, s.Nombre as SectorNombre, u.Id as UsuarioId, u.Username, u.Password FROM dbo.equipos e LEFT JOIN dbo.sectores s ON e.sector_id = s.id LEFT JOIN dbo.usuarios_equipos ue ON e.id = ue.equipo_id LEFT JOIN dbo.usuarios u ON ue.usuario_id = u.id WHERE e.Hostname = @Hostname;"; using (var connection = _context.CreateConnection()) { var equipoDict = new Dictionary(); var equipo = (await connection.QueryAsync( query, (e, sector, usuario) => { if (!equipoDict.TryGetValue(e.Id, out var equipoActual)) { equipoActual = e; equipoActual.Sector = sector; equipoDict.Add(equipoActual.Id, equipoActual); } if (usuario != null && !equipoActual.Usuarios.Any(u => u.Id == usuario.Id)) equipoActual.Usuarios.Add(usuario); return equipoActual; }, new { Hostname = hostname }, splitOn: "SectorId,UsuarioId" )).FirstOrDefault(); if (equipo == null) return NotFound("Equipo no encontrado."); return Ok(equipo); } } // --- POST /api/equipos/{hostname} --- [HttpPost("{hostname}")] public async Task Ingresar(string hostname, [FromBody] Equipo equipoData) { var findQuery = "SELECT * FROM dbo.equipos WHERE Hostname = @Hostname;"; using (var connection = _context.CreateConnection()) { var equipoExistente = await connection.QuerySingleOrDefaultAsync(findQuery, new { Hostname = hostname }); if (equipoExistente == null) { // Crear var insertQuery = @"INSERT INTO dbo.equipos (Hostname, Ip, Mac, Motherboard, Cpu, Ram_installed, Ram_slots, Os, Architecture) VALUES (@Hostname, @Ip, @Mac, @Motherboard, @Cpu, @Ram_installed, @Ram_slots, @Os, @Architecture); SELECT CAST(SCOPE_IDENTITY() as int);"; var nuevoId = await connection.ExecuteScalarAsync(insertQuery, equipoData); equipoData.Id = nuevoId; return CreatedAtAction(nameof(ConsultarDetalle), new { hostname = equipoData.Hostname }, equipoData); } else { // Actualizar y registrar historial var cambios = new Dictionary(); // Comparamos campos para registrar en historial if (equipoData.Ip != equipoExistente.Ip) cambios["ip"] = (equipoExistente.Ip, equipoData.Ip); if (equipoData.Mac != equipoExistente.Mac) cambios["mac"] = (equipoExistente.Mac ?? "", equipoData.Mac ?? ""); var updateQuery = @"UPDATE dbo.equipos SET Ip = @Ip, Mac = @Mac, Motherboard = @Motherboard, Cpu = @Cpu, Ram_installed = @Ram_installed, Ram_slots = @Ram_slots, Os = @Os, Architecture = @Architecture WHERE Hostname = @Hostname;"; await connection.ExecuteAsync(updateQuery, equipoData); if (cambios.Count > 0) { await HistorialHelper.RegistrarCambios(_context, equipoExistente.Id, cambios); } return Ok(equipoData); } } } // --- PUT /api/equipos/{id} --- [HttpPut("{id}")] public async Task Actualizar(int id, [FromBody] Equipo equipoData) { var updateQuery = @"UPDATE dbo.equipos SET Hostname = @Hostname, Ip = @Ip, Mac = @Mac, Motherboard = @Motherboard, Cpu = @Cpu, Ram_installed = @Ram_installed, Ram_slots = @Ram_slots, Os = @Os, Architecture = @Architecture WHERE Id = @Id;"; using (var connection = _context.CreateConnection()) { // Asignamos el ID del parámetro de la ruta al objeto que recibimos. equipoData.Id = id; // Ahora pasamos el objeto completo a Dapper. var filasAfectadas = await connection.ExecuteAsync(updateQuery, equipoData); if (filasAfectadas == 0) return NotFound("Equipo no encontrado."); return NoContent(); } } // --- DELETE /api/equipos/{id} --- [HttpDelete("{id}")] public async Task Borrar(int id) { // La base de datos está configurada con ON DELETE CASCADE, por lo que las relaciones se borrarán automáticamente. var query = "DELETE FROM dbo.equipos WHERE Id = @Id;"; using (var connection = _context.CreateConnection()) { var filasAfectadas = await connection.ExecuteAsync(query, new { Id = id }); if (filasAfectadas == 0) return NotFound("Equipo no encontrado."); return NoContent(); } } // --- GET /api/equipos/{hostname}/historial --- [HttpGet("{hostname}/historial")] public async Task ConsultarHistorial(string hostname) { var query = @"SELECT h.* FROM dbo.historial_equipos h JOIN dbo.equipos e ON h.equipo_id = e.id WHERE e.Hostname = @Hostname ORDER BY h.fecha_cambio DESC;"; using (var connection = _context.CreateConnection()) { var equipo = await connection.QueryFirstOrDefaultAsync("SELECT Id FROM dbo.equipos WHERE Hostname = @Hostname", new { Hostname = hostname }); if (equipo == null) return NotFound("Equipo no encontrado."); var historial = await connection.QueryAsync(query, new { Hostname = hostname }); return Ok(new { equipo = hostname, historial }); } } // --- MÉTODOS DE ASOCIACIÓN Y COMANDOS --- [HttpPatch("{id_equipo}/sector/{id_sector}")] public async Task AsociarSector(int id_equipo, int id_sector) { var query = "UPDATE dbo.equipos SET sector_id = @IdSector WHERE Id = @IdEquipo;"; using (var connection = _context.CreateConnection()) { var filasAfectadas = await connection.ExecuteAsync(query, new { IdSector = id_sector, IdEquipo = id_equipo }); if (filasAfectadas == 0) return NotFound("Equipo o sector no encontrado."); return Ok(new { success = true }); // Devolvemos una respuesta simple de éxito } } [HttpPost("{hostname}/asociarusuario")] public async Task AsociarUsuario(string hostname, [FromBody] AsociacionUsuarioDto dto) { var query = @" INSERT INTO dbo.usuarios_equipos (equipo_id, usuario_id) SELECT e.id, u.id FROM dbo.equipos e, dbo.usuarios u WHERE e.Hostname = @Hostname AND u.Username = @Username;"; using (var connection = _context.CreateConnection()) { try { var filasAfectadas = await connection.ExecuteAsync(query, new { Hostname = hostname, dto.Username }); if (filasAfectadas == 0) return NotFound("Equipo o usuario no encontrado."); return Ok(new { success = true }); } catch (SqlException ex) when (ex.Number == 2627) // Error de clave primaria duplicada { return Conflict("El usuario ya está asociado a este equipo."); } } } [HttpDelete("{hostname}/usuarios/{username}")] public async Task DesasociarUsuario(string hostname, string username) { var query = @" DELETE FROM dbo.usuarios_equipos WHERE equipo_id = (SELECT id FROM dbo.equipos WHERE Hostname = @Hostname) AND usuario_id = (SELECT id FROM dbo.usuarios WHERE Username = @Username);"; using (var connection = _context.CreateConnection()) { var filasAfectadas = await connection.ExecuteAsync(query, new { Hostname = hostname, Username = username }); if (filasAfectadas == 0) return NotFound("Asociación no encontrada."); return Ok(new { success = true }); } } [HttpPost("{hostname}/asociardiscos")] public async Task AsociarDiscos(string hostname, [FromBody] List discosDesdeCliente) { // 1. OBTENER EL EQUIPO var equipoQuery = "SELECT * FROM dbo.equipos WHERE Hostname = @Hostname;"; using var connection = _context.CreateConnection(); connection.Open(); var equipo = await connection.QuerySingleOrDefaultAsync(equipoQuery, new { Hostname = hostname }); if (equipo == null) { return NotFound("Equipo no encontrado."); } // Iniciar una transacción para asegurar que todas las operaciones se completen o ninguna lo haga. using var transaction = connection.BeginTransaction(); try { // 2. OBTENER ASOCIACIONES Y DISCOS ACTUALES DE LA BD var discosActualesQuery = @" SELECT d.Id, d.Mediatype, d.Size, ed.Id as EquipoDiscoId FROM dbo.equipos_discos ed JOIN dbo.discos d ON ed.disco_id = d.id WHERE ed.equipo_id = @EquipoId;"; // Creamos una clase anónima temporal para mapear el resultado del JOIN var discosEnDb = (await connection.QueryAsync(discosActualesQuery, new { EquipoId = equipo.Id }, transaction)).Select(d => new { Id = (int)d.Id, Mediatype = (string)d.Mediatype, Size = (int)d.Size, EquipoDiscoId = (int)d.EquipoDiscoId }).ToList(); // 3. AGRUPAR Y CONTAR DISCOS (del cliente y de la BD) // Crea un diccionario estilo: {"SSD_256": 2, "HDD_1024": 1} var discosClienteContados = discosDesdeCliente .GroupBy(d => $"{d.Mediatype}_{d.Size}") .ToDictionary(g => g.Key, g => g.Count()); var discosDbContados = discosEnDb .GroupBy(d => $"{d.Mediatype}_{d.Size}") .ToDictionary(g => g.Key, g => g.Count()); var cambios = new Dictionary(); // 4. CALCULAR Y EJECUTAR ELIMINACIONES var discosAEliminar = new List(); foreach (var discoDb in discosEnDb) { var key = $"{discoDb.Mediatype}_{discoDb.Size}"; if (discosClienteContados.TryGetValue(key, out int count) && count > 0) { // Este disco todavía existe en el cliente, decrementamos el contador y lo saltamos. discosClienteContados[key]--; } else { // Este disco ya no está en el cliente, marcamos su asociación para eliminar. discosAEliminar.Add(discoDb.EquipoDiscoId); // Registrar para el historial var nombreDisco = $"Disco {discoDb.Mediatype} {discoDb.Size}GB"; var anterior = discosDbContados.GetValueOrDefault(key, 0); if (!cambios.ContainsKey(nombreDisco)) cambios[nombreDisco] = (anterior.ToString(), (anterior - 1).ToString()); else cambios[nombreDisco] = (anterior.ToString(), (int.Parse(cambios[nombreDisco].nuevo) - 1).ToString()); } } if (discosAEliminar.Any()) { await connection.ExecuteAsync("DELETE FROM dbo.equipos_discos WHERE Id IN @Ids;", new { Ids = discosAEliminar }, transaction); } // 5. CALCULAR Y EJECUTAR INSERCIONES foreach (var discoCliente in discosDesdeCliente) { var key = $"{discoCliente.Mediatype}_{discoCliente.Size}"; if (discosDbContados.TryGetValue(key, out int count) && count > 0) { // Este disco ya existía, decrementamos para no volver a añadirlo. discosDbContados[key]--; } else { // Este es un disco nuevo que hay que asociar. var disco = await connection.QuerySingleOrDefaultAsync("SELECT * FROM dbo.discos WHERE Mediatype = @Mediatype AND Size = @Size;", discoCliente, transaction); if (disco == null) continue; // Si el disco no existe en la tabla maestra, lo ignoramos await connection.ExecuteAsync("INSERT INTO dbo.equipos_discos (equipo_id, disco_id) VALUES (@EquipoId, @DiscoId);", new { EquipoId = equipo.Id, DiscoId = disco.Id }, transaction); // Registrar para el historial var nombreDisco = $"Disco {disco.Mediatype} {disco.Size}GB"; var anterior = discosDbContados.GetValueOrDefault(key, 0); if (!cambios.ContainsKey(nombreDisco)) cambios[nombreDisco] = (anterior.ToString(), (anterior + 1).ToString()); else cambios[nombreDisco] = (anterior.ToString(), (int.Parse(cambios[nombreDisco].nuevo) + 1).ToString()); } } // 6. REGISTRAR CAMBIOS Y CONFIRMAR TRANSACCIÓN if (cambios.Count > 0) { // Formateamos los valores para el historial var cambiosFormateados = cambios.ToDictionary( kvp => kvp.Key, kvp => ($"{kvp.Value.anterior} Instalados", $"{kvp.Value.nuevo} Instalados") ); await HistorialHelper.RegistrarCambios(_context, equipo.Id, cambiosFormateados); } transaction.Commit(); return Ok(new { message = "Discos sincronizados correctamente." }); } catch (Exception ex) { transaction.Rollback(); // Loggear el error en el servidor Console.WriteLine($"Error al asociar discos para {hostname}: {ex.Message}"); return StatusCode(500, "Ocurrió un error interno al procesar la solicitud."); } } [HttpPost("{hostname}/ram")] public async Task AsociarRam(string hostname, [FromBody] List memoriasDesdeCliente) { var equipoQuery = "SELECT * FROM dbo.equipos WHERE Hostname = @Hostname;"; using var connection = _context.CreateConnection(); connection.Open(); var equipo = await connection.QuerySingleOrDefaultAsync(equipoQuery, new { Hostname = hostname }); if (equipo == null) { return NotFound("Equipo no encontrado."); } using var transaction = connection.BeginTransaction(); try { // 1. OBTENER ASOCIACIONES DE RAM ACTUALES var ramActualQuery = @" SELECT emr.Id as EquipoMemoriaRamId, emr.Slot, mr.Id, mr.part_number as PartNumber, mr.Fabricante, mr.Tamano, mr.Velocidad FROM dbo.equipos_memorias_ram emr JOIN dbo.memorias_ram mr ON emr.memoria_ram_id = mr.id WHERE emr.equipo_id = @EquipoId;"; var ramEnDb = (await connection.QueryAsync(ramActualQuery, new { EquipoId = equipo.Id }, transaction)).ToList(); // 2. CREAR "HUELLAS DIGITALES" ÚNICAS PARA COMPARAR // Una huella única para cada módulo en un slot. Ej: "DIMM0_Kingston_8_3200" Func crearHuella = ram => $"{ram.Slot}_{ram.PartNumber ?? ""}_{ram.Tamano}_{ram.Velocidad ?? 0}"; var huellasCliente = new HashSet(memoriasDesdeCliente.Select(crearHuella)); var huellasDb = new HashSet(ramEnDb.Select(crearHuella)); // 3. CALCULAR Y EJECUTAR ELIMINACIONES var asociacionesAEliminar = ramEnDb .Where(ramDb => !huellasCliente.Contains(crearHuella(ramDb))) .Select(ramDb => (int)ramDb.EquipoMemoriaRamId) .ToList(); if (asociacionesAEliminar.Any()) { await connection.ExecuteAsync("DELETE FROM dbo.equipos_memorias_ram WHERE Id IN @Ids;", new { Ids = asociacionesAEliminar }, transaction); } // 4. CALCULAR Y EJECUTAR INSERCIONES var memoriasAInsertar = memoriasDesdeCliente.Where(ramCliente => !huellasDb.Contains(crearHuella(ramCliente))).ToList(); foreach (var memInfo in memoriasAInsertar) { // Buscar o crear el módulo de RAM en la tabla maestra 'memorias_ram' var findRamQuery = @"SELECT * FROM dbo.memorias_ram WHERE (part_number = @PartNumber OR (part_number IS NULL AND @PartNumber IS NULL)) AND tamano = @Tamano AND (velocidad = @Velocidad OR (velocidad IS NULL AND @Velocidad IS NULL));"; var memoriaMaestra = await connection.QuerySingleOrDefaultAsync(findRamQuery, memInfo, transaction); int memoriaMaestraId; if (memoriaMaestra == null) { var insertRamQuery = @"INSERT INTO dbo.memorias_ram (part_number, fabricante, tamano, velocidad) VALUES (@PartNumber, @Fabricante, @Tamano, @Velocidad); SELECT CAST(SCOPE_IDENTITY() as int);"; memoriaMaestraId = await connection.ExecuteScalarAsync(insertRamQuery, memInfo, transaction); } else { memoriaMaestraId = memoriaMaestra.Id; } // Crear la asociación en la tabla intermedia var insertAsociacionQuery = "INSERT INTO dbo.equipos_memorias_ram (equipo_id, memoria_ram_id, slot) VALUES (@EquipoId, @MemoriaRamId, @Slot);"; await connection.ExecuteAsync(insertAsociacionQuery, new { EquipoId = equipo.Id, MemoriaRamId = memoriaMaestraId, memInfo.Slot }, transaction); } // (Opcional, pero recomendado) Registrar cambios en el historial. // La lógica exacta para el historial de RAM puede ser compleja y la omitimos por ahora para centrarnos en la sincronización. transaction.Commit(); return Ok(new { message = "Módulos de RAM sincronizados correctamente." }); } catch (Exception ex) { transaction.Rollback(); Console.WriteLine($"Error al asociar RAM para {hostname}: {ex.Message}"); return StatusCode(500, "Ocurrió un error interno al procesar la solicitud de RAM."); } } [HttpPost("ping")] public async Task EnviarPing([FromBody] PingRequestDto request) { if (string.IsNullOrWhiteSpace(request.Ip)) return BadRequest("La dirección IP es requerida."); try { using (var ping = new Ping()) { var reply = await ping.SendPingAsync(request.Ip, 2000); // Timeout de 2 segundos return Ok(new { isAlive = reply.Status == IPStatus.Success, latency = reply.RoundtripTime }); } } catch (PingException ex) { // Maneja errores comunes como "Host desconocido" return Ok(new { isAlive = false, error = ex.Message }); } catch (Exception ex) { return StatusCode(500, $"Error interno al hacer ping: {ex.Message}"); } } // WOL (Wake-On-LAN) es más complejo porque requiere ejecutar comandos de sistema operativo. // Lo dejamos pendiente para no añadir complejidad de configuración de SSH por ahora. [HttpPost("wake-on-lan")] public IActionResult EnviarWol([FromBody] WolRequestDto request) { Console.WriteLine($"Recibida solicitud WOL para MAC: {request.Mac}"); return Ok(new { message = "Solicitud WOL recibida. La ejecución del comando está pendiente de implementación." }); } } // DTOs locales para las peticiones public class PingRequestDto { public string? Ip { get; set; } } public class WolRequestDto { public string? Mac { get; set; } } }