// src/Elecciones.Api/Controllers/ResultadosController.cs using Elecciones.Core.DTOs.ApiResponses; using Elecciones.Database; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Elecciones.Core.DTOs.Configuration; 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) .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); } [HttpGet("composicion-congreso")] public IActionResult GetComposicionCongreso() { // El framework .NET se encarga de leer appsettings.json y mapearlo a nuestras clases. var composicionConfig = _configuration.GetSection("ComposicionCongreso") .Get(); if (composicionConfig == null) { // Devolvemos un error si la sección no se encuentra en el archivo de configuración. return NotFound("La configuración para la composición del congreso no fue encontrada."); } return Ok(composicionConfig); } }