// src/Elecciones.Api/Controllers/ResultadosController.cs using Elecciones.Core.DTOs.ApiResponses; using Elecciones.Database; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Linq; using System.Threading.Tasks; namespace Elecciones.Api.Controllers; [ApiController] [Route("api/[controller]")] public class ResultadosController : ControllerBase { private readonly EleccionesDbContext _dbContext; private readonly ILogger _logger; public ResultadosController(EleccionesDbContext dbContext, ILogger logger) { _dbContext = dbContext; _logger = logger; } [HttpGet("partido/{seccionId}")] public async Task GetResultadosPorPartido(string seccionId) { // 1. Buscamos el ámbito geográfico correspondiente al PARTIDO (Nivel 30) var ambito = await _dbContext.AmbitosGeograficos .AsNoTracking() // CAMBIO CLAVE: Buscamos por SeccionId y NivelId para ser precisos .FirstOrDefaultAsync(a => a.SeccionId == seccionId && a.NivelId == 30); if (ambito == null) { return NotFound(new { message = $"No se encontró el partido con ID {seccionId}" }); } // 2. Buscamos el estado del recuento para ese ámbito var estadoRecuento = await _dbContext.EstadosRecuentos .AsNoTracking() .FirstOrDefaultAsync(e => e.AmbitoGeograficoId == ambito.Id); if (estadoRecuento == null) { // Devolvemos una respuesta vacía pero válida para el frontend return Ok(new MunicipioResultadosDto { MunicipioNombre = ambito.Nombre, UltimaActualizacion = DateTime.UtcNow, PorcentajeEscrutado = 0, PorcentajeParticipacion = 0, Resultados = new List(), VotosAdicionales = new VotosAdicionalesDto() }); } // 3. Buscamos todos los votos para ese ámbito var resultadosVotos = await _dbContext.ResultadosVotos .AsNoTracking() .Include(rv => rv.AgrupacionPolitica) .Where(rv => rv.AmbitoGeograficoId == ambito.Id) .ToListAsync(); // 4. Calculamos el total de votos positivos long totalVotosPositivos = resultadosVotos.Sum(r => r.CantidadVotos); // 5. Mapeamos al DTO de respuesta var respuestaDto = new MunicipioResultadosDto { MunicipioNombre = ambito.Nombre, UltimaActualizacion = estadoRecuento.FechaTotalizacion, PorcentajeEscrutado = estadoRecuento.MesasTotalizadasPorcentaje, PorcentajeParticipacion = estadoRecuento.ParticipacionPorcentaje, Resultados = resultadosVotos.Select(rv => new AgrupacionResultadoDto { Nombre = rv.AgrupacionPolitica.Nombre, Votos = rv.CantidadVotos, Porcentaje = totalVotosPositivos > 0 ? (rv.CantidadVotos * 100.0m / totalVotosPositivos) : 0 }).OrderByDescending(r => r.Votos).ToList(), VotosAdicionales = new VotosAdicionalesDto { EnBlanco = estadoRecuento.VotosEnBlanco, Nulos = estadoRecuento.VotosNulos, Recurridos = estadoRecuento.VotosRecurridos } }; return Ok(respuestaDto); } [HttpGet("provincia/{distritoId}")] public async Task GetResultadosProvinciales(string distritoId) { // TODO: Esta lógica debe ser reemplazada para leer datos reales de la BD // cuando el worker comience a ingestar los totales a nivel provincial. // Por ahora, devolvemos datos simulados para permitir el desarrollo del frontend. var ambito = await _dbContext.AmbitosGeograficos.AsNoTracking() .FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10); if (ambito == null) { return NotFound(new { message = "No se encontró la provincia" }); } // Simulación var random = new Random(); var respuestaSimulada = new ResumenProvincialDto { ProvinciaNombre = ambito.Nombre, UltimaActualizacion = DateTime.UtcNow, PorcentajeEscrutado = 78.45m, PorcentajeParticipacion = 65.12m, Resultados = [ new() { Nombre = "ALIANZA POR EL FUTURO", Votos = 2500000 + random.Next(1, 1000), Porcentaje = 45.12m }, new() { Nombre = "FRENTE DE AVANZADA", Votos = 2100000 + random.Next(1, 1000), Porcentaje = 38.78m }, new() { Nombre = "UNION POPULAR", Votos = 800000 + random.Next(1, 1000), Porcentaje = 14.10m }, ], VotosAdicionales = new VotosAdicionalesDto { EnBlanco = 150000, Nulos = 80000, Recurridos = 1200 } }; return Ok(await Task.FromResult(respuestaSimulada)); } [HttpGet("bancas/{seccionId}")] public async Task GetBancasPorSeccion(string seccionId) { // 1. Buscamos el ámbito de la sección electoral var seccion = await _dbContext.AmbitosGeograficos .AsNoTracking() .FirstOrDefaultAsync(a => a.SeccionId == seccionId && a.NivelId == 4); // Nivel 4 = Sección Electoral if (seccion == null) { return NotFound(new { message = $"No se encontró la sección electoral con ID {seccionId}" }); } // 2. Buscamos todas las proyecciones para ese ámbito, incluyendo el nombre de la agrupación var proyecciones = await _dbContext.ProyeccionesBancas .AsNoTracking() .Include(p => p.AgrupacionPolitica) // Incluimos el nombre del partido .Where(p => p.AmbitoGeograficoId == seccion.Id) .Select(p => new { // Creamos un objeto anónimo para la respuesta, más limpio que un DTO para este caso simple AgrupacionNombre = p.AgrupacionPolitica.Nombre, Bancas = p.NroBancas }) .OrderByDescending(p => p.Bancas) .ToListAsync(); if (!proyecciones.Any()) { return NotFound(new { message = $"No se han encontrado proyecciones de bancas para la sección {seccion.Nombre}" }); } // 3. Devolvemos un objeto que contiene el nombre de la sección y la lista de resultados return Ok(new { SeccionNombre = seccion.Nombre, Proyeccion = proyecciones }); } [HttpGet("mapa")] public async Task GetResultadosParaMapa() { var maxVotosPorAmbito = _dbContext.ResultadosVotos .GroupBy(rv => rv.AmbitoGeograficoId) .Select(g => new { AmbitoId = g.Key, MaxVotos = g.Max(v => v.CantidadVotos) }); var resultadosGanadores = await _dbContext.ResultadosVotos .Join( maxVotosPorAmbito, voto => new { AmbitoId = voto.AmbitoGeograficoId, Votos = voto.CantidadVotos }, max => new { AmbitoId = max.AmbitoId, Votos = max.MaxVotos }, (voto, max) => voto ) .Include(rv => rv.AmbitoGeografico) .Where(rv => rv.AmbitoGeografico.NivelId == 30) // Aseguramos que solo sean los ámbitos de nivel 30 .Select(rv => new { // CORRECCIÓN CLAVE: Devolvemos los campos que el frontend necesita para funcionar. // 1. El ID de la BD para hacer clic y pedir detalles. AmbitoId = rv.AmbitoGeografico.Id, // 2. El NOMBRE del departamento/municipio para encontrar y colorear el polígono. DepartamentoNombre = rv.AmbitoGeografico.Nombre, // 3. El ID del partido ganador. AgrupacionGanadoraId = rv.AgrupacionPoliticaId }) .ToListAsync(); return Ok(resultadosGanadores); } [HttpGet("municipio/{ambitoId}")] // Cambiamos el nombre del parámetro de ruta public async Task GetResultadosPorMunicipio(int ambitoId) // Cambiamos el tipo de string a int { _logger.LogInformation("Buscando resultados para AmbitoGeograficoId: {AmbitoId}", ambitoId); // PASO 1: Buscar el Ámbito Geográfico directamente por su CLAVE PRIMARIA (AmbitoGeograficoId). var ambito = await _dbContext.AmbitosGeograficos .AsNoTracking() .FirstOrDefaultAsync(a => a.Id == ambitoId && a.NivelId == 30); // Usamos a.Id == ambitoId if (ambito == null) { _logger.LogWarning("No se encontró el ámbito para el ID interno: {AmbitoId} o no es Nivel 30.", ambitoId); return NotFound(new { message = $"No se encontró el municipio con ID interno {ambitoId}" }); } _logger.LogInformation("Ámbito encontrado: Id={AmbitoId}, Nombre={AmbitoNombre}", ambito.Id, ambito.Nombre); // PASO 2: Usar la CLAVE PRIMARIA (ambito.Id) para buscar el estado del recuento. var estadoRecuento = await _dbContext.EstadosRecuentos .AsNoTracking() .FirstOrDefaultAsync(e => e.AmbitoGeograficoId == ambito.Id); if (estadoRecuento == null) { _logger.LogWarning("No se encontró EstadoRecuento para AmbitoGeograficoId: {AmbitoId}", ambito.Id); return NotFound(new { message = $"No se han encontrado resultados de recuento para el municipio {ambito.Nombre}" }); } // PASO 3: Usar la CLAVE PRIMARIA (ambito.Id) para buscar los votos. var resultadosVotos = await _dbContext.ResultadosVotos .AsNoTracking() .Include(rv => rv.AgrupacionPolitica) // Incluimos el nombre del partido .Where(rv => rv.AmbitoGeograficoId == ambito.Id) .OrderByDescending(rv => rv.CantidadVotos) .ToListAsync(); // PASO 4: Calcular el total de votos positivos para el porcentaje. long totalVotosPositivos = resultadosVotos.Sum(r => r.CantidadVotos); // PASO 5: Mapear todo al DTO de respuesta que el frontend espera. var respuestaDto = new MunicipioResultadosDto { MunicipioNombre = ambito.Nombre, UltimaActualizacion = estadoRecuento.FechaTotalizacion, PorcentajeEscrutado = estadoRecuento.MesasTotalizadasPorcentaje, PorcentajeParticipacion = estadoRecuento.ParticipacionPorcentaje, Resultados = resultadosVotos.Select(rv => new AgrupacionResultadoDto { Nombre = rv.AgrupacionPolitica.Nombre, Votos = rv.CantidadVotos, Porcentaje = totalVotosPositivos > 0 ? (rv.CantidadVotos * 100.0m / totalVotosPositivos) : 0 }).ToList(), VotosAdicionales = new VotosAdicionalesDto { EnBlanco = estadoRecuento.VotosEnBlanco, Nulos = estadoRecuento.VotosNulos, Recurridos = estadoRecuento.VotosRecurridos } }; return Ok(respuestaDto); } }