// src/Elecciones.Api/Controllers/ResultadosController.cs using Elecciones.Core.DTOs.ApiResponses; using Elecciones.Database; using Elecciones.Database.Entities; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Elecciones.Api.Controllers; [ApiController] [Route("api/[controller]")] public class ResultadosController : ControllerBase { private readonly EleccionesDbContext _dbContext; private readonly ILogger _logger; private readonly IConfiguration _configuration; public ResultadosController(EleccionesDbContext dbContext, ILogger logger, IConfiguration configuration) { _dbContext = dbContext; _logger = logger; _configuration = configuration; } [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) { _logger.LogInformation("Solicitud de resultados para la provincia con distritoId: {DistritoId}", distritoId); // PASO 1: Encontrar el ámbito geográfico de la provincia. var provincia = await _dbContext.AmbitosGeograficos.AsNoTracking() .FirstOrDefaultAsync(a => a.DistritoId == distritoId && a.NivelId == 10); if (provincia == null) { _logger.LogWarning("No se encontró la provincia con distritoId: {DistritoId}", distritoId); return NotFound(new { message = $"No se encontró la provincia con distritoId {distritoId}" }); } // PASO 2: Obtener el estado general del recuento para la provincia. // Como las estadísticas generales (mesas, participación) son las mismas para todas las categorías, // simplemente tomamos la primera que encontremos para este ámbito. var estadoGeneral = await _dbContext.EstadosRecuentosGenerales.AsNoTracking() .FirstOrDefaultAsync(e => e.AmbitoGeograficoId == provincia.Id); // PASO 3: Obtener el resumen de votos por agrupación para la provincia. // Hacemos un JOIN manual entre ResumenesVotos y AgrupacionesPoliticas para obtener los nombres. var resultados = await _dbContext.ResumenesVotos .AsNoTracking() .Where(r => r.AmbitoGeograficoId == provincia.Id) .Join( _dbContext.AgrupacionesPoliticas.AsNoTracking(), resumen => resumen.AgrupacionPoliticaId, agrupacion => agrupacion.Id, (resumen, agrupacion) => new AgrupacionResultadoDto { Nombre = agrupacion.Nombre, Votos = resumen.Votos, Porcentaje = resumen.VotosPorcentaje }) .OrderByDescending(r => r.Votos) .ToListAsync(); // PASO 4: Construir el objeto de respuesta (DTO). // Si no hay datos de recuento aún, usamos valores por defecto para evitar errores en el frontend. var respuestaDto = new ResumenProvincialDto { ProvinciaNombre = provincia.Nombre, UltimaActualizacion = estadoGeneral?.FechaTotalizacion ?? DateTime.UtcNow, PorcentajeEscrutado = estadoGeneral?.MesasTotalizadasPorcentaje ?? 0, PorcentajeParticipacion = estadoGeneral?.ParticipacionPorcentaje ?? 0, Resultados = resultados, // NOTA: Los votos adicionales (nulos, en blanco) no están en la tabla de resumen provincial. // Esto es una mejora pendiente en el Worker. Por ahora, devolvemos 0. VotosAdicionales = new VotosAdicionalesDto { EnBlanco = 0, Nulos = 0, Recurridos = 0 } }; _logger.LogInformation("Devolviendo {NumResultados} resultados de agrupaciones para la provincia.", respuestaDto.Resultados.Count); return Ok(respuestaDto); } [HttpGet("bancas/{seccionId}")] public async Task GetBancasPorSeccion(string seccionId) { // 1. Buscamos el ámbito usando 'SeccionProvincialId'. // La API oficial usa este campo para las secciones electorales. // Además, el worker guarda estas secciones con NivelId = 20, por lo que lo usamos aquí para consistencia. var seccion = await _dbContext.AmbitosGeograficos .AsNoTracking() .FirstOrDefaultAsync(a => a.SeccionProvincialId == seccionId && a.NivelId == 20); // Nivel 20 = Sección Electoral Provincial if (seccion == null) { _logger.LogWarning("No se encontró la sección electoral con SeccionProvincialId: {SeccionId}", seccionId); return NotFound(new { message = $"No se encontró la sección electoral con ID {seccionId}" }); } // 2. Buscamos todas las proyecciones para ese ámbito (usando su clave primaria 'Id') var proyecciones = await _dbContext.ProyeccionesBancas .AsNoTracking() .Include(p => p.AgrupacionPolitica) .Where(p => p.AmbitoGeograficoId == seccion.Id) .Select(p => new { AgrupacionNombre = p.AgrupacionPolitica.Nombre, Bancas = p.NroBancas }) .OrderByDescending(p => p.Bancas) .ToListAsync(); if (!proyecciones.Any()) { // Este caso es posible si aún no hay proyecciones calculadas para esta sección. _logger.LogWarning("No se encontraron proyecciones de bancas para la sección: {SeccionNombre}", seccion.Nombre); return NotFound(new { message = $"No se han encontrado proyecciones de bancas para la sección {seccion.Nombre}" }); } // 3. Devolvemos la respuesta 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) .Include(rv => rv.AgrupacionPolitica) .Where(rv => rv.AmbitoGeografico.NivelId == 30) .Select(rv => new { AmbitoId = rv.AmbitoGeografico.Id, DepartamentoNombre = rv.AmbitoGeografico.Nombre, AgrupacionGanadoraId = rv.AgrupacionPoliticaId, ColorGanador = rv.AgrupacionPolitica.Color }) .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); } [HttpGet("composicion-congreso")] public async Task GetComposicionCongreso() { var config = await _dbContext.Configuraciones .AsNoTracking() .ToDictionaryAsync(c => c.Clave, c => c.Valor); config.TryGetValue("UsarDatosDeBancadasOficiales", out var usarDatosOficialesValue); bool usarDatosOficiales = usarDatosOficialesValue == "true"; if (usarDatosOficiales) { return await GetComposicionDesdeBancadasOficiales(config); } else { return await GetComposicionDesdeProyecciones(config); } } private async Task GetComposicionDesdeBancadasOficiales(Dictionary config) { config.TryGetValue("MostrarOcupantes", out var mostrarOcupantesValue); bool mostrarOcupantes = mostrarOcupantesValue == "true"; // Se declara la variable explícitamente como IQueryable IQueryable bancadasQuery = _dbContext.Bancadas.AsNoTracking() .Include(b => b.AgrupacionPolitica); if (mostrarOcupantes) { // Ahora sí podemos añadir otro .Include() sin problemas de tipo bancadasQuery = bancadasQuery.Include(b => b.Ocupante); } var bancadas = await bancadasQuery.ToListAsync(); // --- CAMBIO 2: Eliminar la carga manual de Ocupantes --- // Ya no necesitamos 'ocupantesLookup'. Se puede borrar todo este bloque: /* var ocupantesLookup = new Dictionary(); if (mostrarOcupantes) { ocupantesLookup = (await _dbContext.OcupantesBancas.AsNoTracking() .ToListAsync()) .ToDictionary(o => o.BancadaId); } */ var bancasPorAgrupacion = bancadas .Where(b => b.AgrupacionPoliticaId != null) .GroupBy(b => new { b.AgrupacionPoliticaId, b.Camara }) .Select(g => new { Agrupacion = g.First().AgrupacionPolitica, g.Key.Camara, BancasTotales = g.Count() }) .ToList(); var presidenteDiputados = bancasPorAgrupacion .Where(b => b.Camara == Core.Enums.TipoCamara.Diputados) .OrderByDescending(b => b.BancasTotales) .FirstOrDefault()?.Agrupacion; config.TryGetValue("PresidenciaSenadores", out var idPartidoPresidenteSenadores); var presidenteSenadores = await _dbContext.AgrupacionesPoliticas.FindAsync(idPartidoPresidenteSenadores); object MapearPartidos(Core.Enums.TipoCamara camara) { // 1. Filtramos las bancadas que nos interesan (por cámara y que tengan partido). var bancadasDeCamara = bancadas .Where(b => b.Camara == camara && b.AgrupacionPolitica != null); // 2. --- ¡EL CAMBIO CLAVE ESTÁ AQUÍ! --- // Agrupamos por el ID de la Agrupación, no por el objeto. // Esto garantiza que todas las bancadas del mismo partido terminen en el MISMO grupo. var partidosDeCamara = bancadasDeCamara .GroupBy(b => b.AgrupacionPolitica!.Id) .Select(g => new { // La Agrupacion la podemos tomar del primer elemento del grupo, // ya que todas las bancadas del grupo pertenecen al mismo partido. Agrupacion = g.First().AgrupacionPolitica!, // g ahora contiene la lista COMPLETA de bancadas para esta agrupación. BancasDelPartido = g.ToList() }); // 3. Ordenamos, como antes, pero ahora sobre una lista de grupos correcta. var partidosOrdenados = (camara == Core.Enums.TipoCamara.Diputados) ? partidosDeCamara.OrderBy(p => p.Agrupacion.OrdenDiputados ?? 999) : partidosDeCamara.OrderBy(p => p.Agrupacion.OrdenSenadores ?? 999); // 4. Mapeamos al resultado final. return partidosOrdenados .ThenByDescending(p => p.BancasDelPartido.Count) .Select(p => { // Ahora 'p.BancasDelPartido' contiene TODAS las bancadas del partido (en tu caso, las 2). // Cuando hagamos el .Select() aquí, recorrerá ambas y encontrará a los ocupantes. var ocupantesDelPartido = p.BancasDelPartido .Select(b => b.Ocupante) .Where(o => o != null) .ToList(); return new { p.Agrupacion.Id, p.Agrupacion.Nombre, p.Agrupacion.NombreCorto, p.Agrupacion.Color, BancasTotales = p.BancasDelPartido.Count, // ¡Esta lista ahora debería contener a tus 2 ocupantes! Ocupantes = mostrarOcupantes ? ocupantesDelPartido : new List() }; }).ToList(); } // El resto del método permanece igual... var diputados = new { CamaraNombre = "Cámara de Diputados", TotalBancas = 92, BancasEnJuego = 0, Partidos = MapearPartidos(Core.Enums.TipoCamara.Diputados), PresidenteBancada = presidenteDiputados != null ? new { presidenteDiputados.Color } : null }; var senadores = new { CamaraNombre = "Cámara de Senadores", TotalBancas = 46, BancasEnJuego = 0, Partidos = MapearPartidos(Core.Enums.TipoCamara.Senadores), PresidenteBancada = presidenteSenadores != null ? new { presidenteSenadores.Color } : null }; return Ok(new { Diputados = diputados, Senadores = senadores }); } private async Task GetComposicionDesdeProyecciones(Dictionary config) { var bancasPorAgrupacion = await _dbContext.ProyeccionesBancas .AsNoTracking() .GroupBy(p => new { p.AgrupacionPoliticaId, p.CategoriaId }) .Select(g => new { AgrupacionId = g.Key.AgrupacionPoliticaId, CategoriaId = g.Key.CategoriaId, BancasTotales = g.Sum(p => p.NroBancas) }) .ToListAsync(); var todasAgrupaciones = await _dbContext.AgrupacionesPoliticas.AsNoTracking().ToDictionaryAsync(a => a.Id); config.TryGetValue("PresidenciaSenadores", out var idPartidoPresidenteSenadores); todasAgrupaciones.TryGetValue(idPartidoPresidenteSenadores ?? "", out var presidenteSenadores); string? idPartidoPresidenteDiputados = bancasPorAgrupacion .Where(b => b.CategoriaId == 6) .OrderByDescending(b => b.BancasTotales) .FirstOrDefault()?.AgrupacionId; todasAgrupaciones.TryGetValue(idPartidoPresidenteDiputados ?? "", out var presidenteDiputados); object MapearPartidos(int categoriaId) { var partidosDeCamara = bancasPorAgrupacion .Where(b => b.CategoriaId == categoriaId && b.BancasTotales > 0) .Select(b => new { Bancas = b, Agrupacion = todasAgrupaciones[b.AgrupacionId] }); if (categoriaId == 6) // Diputados partidosDeCamara = partidosDeCamara.OrderBy(b => b.Agrupacion.OrdenDiputados ?? 999) .ThenByDescending(b => b.Bancas.BancasTotales); else // Senadores partidosDeCamara = partidosDeCamara.OrderBy(b => b.Agrupacion.OrdenSenadores ?? 999) .ThenByDescending(b => b.Bancas.BancasTotales); return partidosDeCamara .Select(b => new { b.Agrupacion.Id, b.Agrupacion.Nombre, b.Agrupacion.NombreCorto, b.Agrupacion.Color, b.Bancas.BancasTotales }) .ToList(); } var diputados = new { CamaraNombre = "Cámara de Diputados", TotalBancas = 92, BancasEnJuego = 46, Partidos = MapearPartidos(6), PresidenteBancada = presidenteDiputados != null ? new { presidenteDiputados.Color } : null }; var senadores = new { CamaraNombre = "Cámara de Senadores", TotalBancas = 46, BancasEnJuego = 23, Partidos = MapearPartidos(5), PresidenteBancada = presidenteSenadores != null ? new { presidenteSenadores.Color } : null }; return Ok(new { Diputados = diputados, Senadores = senadores }); } [HttpGet("bancadas-detalle")] public async Task GetBancadasConOcupantes() { var config = await _dbContext.Configuraciones.AsNoTracking().ToDictionaryAsync(c => c.Clave, c => c.Valor); config.TryGetValue("MostrarOcupantes", out var mostrarOcupantesValue); if (mostrarOcupantesValue != "true") { // Si la opción está desactivada, devolvemos un array vacío. return Ok(new List()); } var bancadasConOcupantes = await _dbContext.Bancadas .AsNoTracking() .Include(b => b.Ocupante) .Where(b => b.Ocupante != null) // Solo las que tienen un ocupante asignado .Select(b => new { b.Id, b.Camara, b.AgrupacionPoliticaId, Ocupante = b.Ocupante }) .ToListAsync(); return Ok(bancadasConOcupantes); } }