//Elecciones.Worker/LowPriorityDataWorker.cs
using Elecciones.Database;
using Elecciones.Database.Entities;
using Elecciones.Infrastructure.Services;
using Microsoft.EntityFrameworkCore;

namespace Elecciones.Worker;

public class LowPriorityDataWorker : BackgroundService
{
  private readonly ILogger<LowPriorityDataWorker> _logger;
  private readonly SharedTokenService _tokenService;
  private readonly IServiceProvider _serviceProvider;
  private readonly IElectoralApiService _apiService;
  private readonly WorkerConfigService _configService;

  // Una variable para rastrear la tarea de telegramas, si está en ejecución.
  private Task? _telegramasTask;

  public LowPriorityDataWorker(
  ILogger<LowPriorityDataWorker> logger,
  SharedTokenService tokenService,
  IServiceProvider serviceProvider,
  IElectoralApiService apiService,
  WorkerConfigService configService)
  {
    _logger = logger;
    _tokenService = tokenService;
    _serviceProvider = serviceProvider;
    _apiService = apiService;
    _configService = configService;
  }

  protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  {
    _logger.LogInformation("Worker de Baja Prioridad iniciado.");

    // La sincronización inicial sigue siendo un paso de bloqueo, es necesario.
    await SincronizarCatalogosMaestrosAsync(stoppingToken);

    while (!stoppingToken.IsCancellationRequested)
    {
      _logger.LogInformation("--- Iniciando Ciclo de Datos de Baja Prioridad ---");

      var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken);
      if (string.IsNullOrEmpty(authToken))
      {
        _logger.LogError("Ciclo de Baja Prioridad: No se pudo obtener token. Reintentando en 1 minuto.");
        await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        continue;
      }

      var settings = await _configService.GetSettingsAsync();

      if (settings.Prioridad == "Telegramas" && settings.ResultadosActivado)
      {
        _logger.LogInformation("Ejecutando tareas de Resultados en baja prioridad.");
        await SondearResultadosMunicipalesAsync(authToken, stoppingToken);
        await SondearResumenProvincialAsync(authToken, stoppingToken);
        await SondearEstadoRecuentoGeneralAsync(authToken, stoppingToken);
      }
      else if (settings.Prioridad == "Resultados" && settings.BajasActivado)
      {
        _logger.LogInformation("Ejecutando tareas de Baja Prioridad en baja prioridad.");
        await SondearProyeccionBancasAsync(authToken, stoppingToken);
        await SondearNuevosTelegramasAsync(authToken, stoppingToken);
      }
      else
      {
        _logger.LogInformation("Worker de baja prioridad inactivo según la configuración.");
      }
      try
      {
        await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
      }
      catch (TaskCanceledException)
      {
        break;
      }
    }
  }

  private async Task SondearResultadosMunicipalesAsync(string authToken, CancellationToken stoppingToken)
  {
    try
    {
      using var scope = _serviceProvider.CreateScope();
      var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();

      var municipiosASondear = await dbContext.AmbitosGeograficos
          .AsNoTracking()
          .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null)
          .ToListAsync(stoppingToken);

      var todasLasCategorias = await dbContext.CategoriasElectorales
          .AsNoTracking()
          .ToListAsync(stoppingToken);

      if (!municipiosASondear.Any() || !todasLasCategorias.Any())
      {
        _logger.LogWarning("No se encontraron Partidos (NivelId 30) o Categorías para sondear resultados.");
        return;
      }

      _logger.LogInformation("Iniciando sondeo de resultados para {m} municipios y {c} categorías...", municipiosASondear.Count, todasLasCategorias.Count);

      foreach (var municipio in municipiosASondear)
      {
        if (stoppingToken.IsCancellationRequested) break;

        var tareasCategoria = todasLasCategorias.Select(async categoria =>
        {
          var resultados = await _apiService.GetResultadosAsync(authToken, municipio.DistritoId!, municipio.SeccionId!, null, categoria.Id);

          if (resultados != null)
          {
            using var innerScope = _serviceProvider.CreateScope();
            var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>();

            // --- LLAMADA CORRECTA ---
            await GuardarResultadosDeAmbitoAsync(innerDbContext, municipio.Id, categoria.Id, resultados, stoppingToken);
          }
        });

        await Task.WhenAll(tareasCategoria);
      }
    }
    catch (Exception ex)
    {
      _logger.LogError(ex, "Ocurrió un error inesperado durante el sondeo de resultados municipales.");
    }
  }

  private async Task GuardarResultadosDeAmbitoAsync(
      EleccionesDbContext dbContext, int ambitoId, int categoriaId,
      Elecciones.Core.DTOs.ResultadosDto resultadosDto, CancellationToken stoppingToken)
  {
    var estadoRecuento = await dbContext.EstadosRecuentos.FindAsync(new object[] { ambitoId, categoriaId }, stoppingToken);

    if (estadoRecuento == null)
    {
      estadoRecuento = new EstadoRecuento { AmbitoGeograficoId = ambitoId, CategoriaId = categoriaId };
      dbContext.EstadosRecuentos.Add(estadoRecuento);
    }

    estadoRecuento.FechaTotalizacion = DateTime.Parse(resultadosDto.FechaTotalizacion).ToUniversalTime();
    estadoRecuento.MesasEsperadas = resultadosDto.EstadoRecuento.MesasEsperadas;
    estadoRecuento.MesasTotalizadas = resultadosDto.EstadoRecuento.MesasTotalizadas;
    estadoRecuento.MesasTotalizadasPorcentaje = resultadosDto.EstadoRecuento.MesasTotalizadasPorcentaje;
    estadoRecuento.CantidadElectores = resultadosDto.EstadoRecuento.CantidadElectores;
    estadoRecuento.CantidadVotantes = resultadosDto.EstadoRecuento.CantidadVotantes;
    estadoRecuento.ParticipacionPorcentaje = resultadosDto.EstadoRecuento.ParticipacionPorcentaje;

    if (resultadosDto.ValoresTotalizadosOtros != null)
    {
      estadoRecuento.VotosEnBlanco = resultadosDto.ValoresTotalizadosOtros.VotosEnBlanco;
      estadoRecuento.VotosEnBlancoPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosEnBlancoPorcentaje;
      estadoRecuento.VotosNulos = resultadosDto.ValoresTotalizadosOtros.VotosNulos;
      estadoRecuento.VotosNulosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosNulosPorcentaje;
      estadoRecuento.VotosRecurridos = resultadosDto.ValoresTotalizadosOtros.VotosRecurridos;
      estadoRecuento.VotosRecurridosPorcentaje = resultadosDto.ValoresTotalizadosOtros.VotosRecurridosPorcentaje;
    }

    foreach (var votoPositivoDto in resultadosDto.ValoresTotalizadosPositivos)
    {
      var resultadoVoto = await dbContext.ResultadosVotos.FirstOrDefaultAsync(
          rv => rv.AmbitoGeograficoId == ambitoId &&
                rv.CategoriaId == categoriaId &&
                rv.AgrupacionPoliticaId == votoPositivoDto.IdAgrupacion,
          stoppingToken
      );

      if (resultadoVoto == null)
      {
        resultadoVoto = new ResultadoVoto
        {
          AmbitoGeograficoId = ambitoId,
          CategoriaId = categoriaId,
          AgrupacionPoliticaId = votoPositivoDto.IdAgrupacion
        };
        dbContext.ResultadosVotos.Add(resultadoVoto);
      }
      resultadoVoto.CantidadVotos = votoPositivoDto.Votos;
      resultadoVoto.PorcentajeVotos = votoPositivoDto.VotosPorcentaje;
    }

    try
    {
      await dbContext.SaveChangesAsync(stoppingToken);
    }
    catch (DbUpdateException ex)
    {
      _logger.LogError(ex, "DbUpdateException al guardar resultados para AmbitoId {ambitoId} y CategoriaId {categoriaId}", ambitoId, categoriaId);
    }
  }

  /// <summary>
  /// Obtiene y actualiza el resumen de votos y el estado del recuento a nivel provincial.
  /// Esta versión actualizada guarda tanto los votos por agrupación (en ResumenesVotos)
  /// como el estado general del recuento, incluyendo la fecha de totalización (en EstadosRecuentosGenerales),
  /// asegurando que toda la operación sea atómica mediante una transacción de base de datos.
  /// </summary>
  /// <param name="authToken">El token de autenticación válido para la sesión.</param>
  /// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
  private async Task SondearResumenProvincialAsync(string authToken, CancellationToken stoppingToken)
  {
    try
    {
      // Creamos un scope de DbContext para esta operación.
      using var scope = _serviceProvider.CreateScope();
      var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();

      // Obtenemos el registro de la Provincia (NivelId 10).
      var provincia = await dbContext.AmbitosGeograficos
          .AsNoTracking()
          .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken);

      // Si no encontramos el ámbito de la provincia, no podemos continuar.
      if (provincia == null)
      {
        _logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) para el sondeo de resumen.");
        return;
      }

      // Llamamos a la API para obtener el resumen de datos provincial.
      var resumenDto = await _apiService.GetResumenAsync(authToken, provincia.DistritoId!);

      // Solo procedemos si la API devolvió una respuesta válida y no nula.
      if (resumenDto != null)
      {
        // Iniciamos una transacción explícita. Esto garantiza que todas las operaciones de base de datos
        // dentro de este bloque (el DELETE, los INSERTs y los UPDATEs) se completen con éxito,
        // o si algo falla, se reviertan todas, manteniendo la consistencia de los datos.
        await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken);

        // --- 1. ACTUALIZAR LA TABLA 'ResumenesVotos' ---

        // Verificamos si la respuesta contiene una lista de votos positivos.
        if (resumenDto.ValoresTotalizadosPositivos is { Count: > 0 } nuevosVotos)
        {
          // Estrategia "Borrar y Reemplazar": vaciamos la tabla antes de insertar los nuevos datos.
          await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ResumenesVotos", stoppingToken);

          // Añadimos cada nuevo registro de voto al DbContext.
          foreach (var voto in nuevosVotos)
          {
            dbContext.ResumenesVotos.Add(new ResumenVoto
            {
              AmbitoGeograficoId = provincia.Id,
              AgrupacionPoliticaId = voto.IdAgrupacion,
              Votos = voto.Votos,
              VotosPorcentaje = voto.VotosPorcentaje
            });
          }
        }

        // --- 2. ACTUALIZAR LA TABLA 'EstadosRecuentosGenerales' ---

        // El endpoint de Resumen no especifica una categoría, por lo que aplicamos sus datos de estado de recuento
        // a todas las categorías que tenemos en nuestra base de datos.
        var todasLasCategorias = await dbContext.CategoriasElectorales.AsNoTracking().ToListAsync(stoppingToken);
        foreach (var categoria in todasLasCategorias)
        {
          // Buscamos el registro existente usando la clave primaria compuesta.
          var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(new object[] { provincia.Id, categoria.Id }, stoppingToken);

          // Si no existe, lo creamos.
          if (registroDb == null)
          {
            registroDb = new EstadoRecuentoGeneral { AmbitoGeograficoId = provincia.Id, CategoriaId = categoria.Id };
            dbContext.EstadosRecuentosGenerales.Add(registroDb);
          }

          // Parseamos la fecha de forma segura para evitar errores con cadenas vacías o nulas.
          if (DateTime.TryParse(resumenDto.FechaTotalizacion, out var parsedDate))
          {
            registroDb.FechaTotalizacion = parsedDate.ToUniversalTime();
          }

          // Mapeamos el resto de los datos del estado del recuento.
          registroDb.MesasEsperadas = resumenDto.EstadoRecuento.MesasEsperadas;
          registroDb.MesasTotalizadas = resumenDto.EstadoRecuento.MesasTotalizadas;
          registroDb.MesasTotalizadasPorcentaje = resumenDto.EstadoRecuento.MesasTotalizadasPorcentaje;
          registroDb.CantidadElectores = resumenDto.EstadoRecuento.CantidadElectores;
          registroDb.CantidadVotantes = resumenDto.EstadoRecuento.CantidadVotantes;
          registroDb.ParticipacionPorcentaje = resumenDto.EstadoRecuento.ParticipacionPorcentaje;
        }

        // 3. CONFIRMAR Y GUARDAR
        // Guardamos todos los cambios preparados (DELETEs, INSERTs, UPDATEs) en la base de datos.
        await dbContext.SaveChangesAsync(stoppingToken);
        // Confirmamos la transacción para hacer los cambios permanentes.
        await transaction.CommitAsync(stoppingToken);

        _logger.LogInformation("Sondeo de Resumen Provincial completado. Las tablas han sido actualizadas.");
      }
      else
      {
        // Si la API no devolvió datos (ej. devuelve null), no hacemos nada en la BD.
        _logger.LogInformation("Sondeo de Resumen Provincial completado. No se recibieron datos nuevos.");
      }
    }
    catch (OperationCanceledException)
    {
      _logger.LogInformation("Sondeo de resumen provincial cancelado.");
    }
    catch (Exception ex)
    {
      // Capturamos cualquier otro error inesperado para que el worker no se detenga.
      _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Resumen Provincial.");
    }
  }

  /// <summary>
  /// Obtiene y actualiza el estado general del recuento a nivel provincial para CADA categoría electoral.
  /// Esta versión es robusta: consulta dinámicamente las categorías, usa la clave primaria compuesta
  /// de la base de datos y guarda todos los cambios en una única transacción al final.
  /// </summary>
  /// <param name="authToken">El token de autenticación válido para la sesión.</param>
  /// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
  private async Task SondearEstadoRecuentoGeneralAsync(string authToken, CancellationToken stoppingToken)
  {
    try
    {
      // PASO 1: Crear un "scope" para obtener una instancia fresca de DbContext.
      // Esto es una práctica recomendada para servicios de larga duración para evitar problemas de concurrencia.
      using var scope = _serviceProvider.CreateScope();
      var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();

      // PASO 2: Obtener el ámbito geográfico de la Provincia.
      // Necesitamos este objeto para obtener su 'DistritoId' ("02"), que es requerido por la API.
      var provincia = await dbContext.AmbitosGeograficos
          .AsNoTracking() // Optimización: Solo necesitamos leer datos, no modificarlos.
          .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken);

      // Comprobación de seguridad: Si la sincronización inicial falló y no tenemos el registro de la provincia,
      // no podemos continuar. Registramos una advertencia y salimos del método.
      if (provincia == null)
      {
        _logger.LogWarning("No se encontró el ámbito 'Provincia' (NivelId 10) en la BD. Omitiendo sondeo de estado general.");
        return;
      }

      // PASO 3: Obtener todas las categorías electorales disponibles desde nuestra base de datos.
      // Esto hace que el método sea dinámico y no dependa de IDs fijos en el código.
      var categoriasParaSondear = await dbContext.CategoriasElectorales
          .AsNoTracking()
          .ToListAsync(stoppingToken);

      if (!categoriasParaSondear.Any())
      {
        _logger.LogWarning("No hay categorías en la BD para sondear el estado general del recuento.");
        return;
      }

      _logger.LogInformation("Iniciando sondeo de Estado Recuento General para {count} categorías...", categoriasParaSondear.Count);

      // PASO 4: Iterar sobre cada categoría para obtener su estado de recuento individual.
      foreach (var categoria in categoriasParaSondear)
      {
        // Salimos limpiamente del bucle si la aplicación se está deteniendo.
        if (stoppingToken.IsCancellationRequested) break;

        // Llamamos a la API con el distrito y la CATEGORÍA ACTUAL del bucle.
        var estadoDto = await _apiService.GetEstadoRecuentoGeneralAsync(authToken, provincia.DistritoId!, categoria.Id);

        // Solo procedemos si la API devolvió datos válidos.
        if (estadoDto != null)
        {
          // Lógica "Upsert" (Update or Insert):
          // Buscamos un registro existente usando la CLAVE PRIMARIA COMPUESTA.
          var registroDb = await dbContext.EstadosRecuentosGenerales.FindAsync(
              new object[] { provincia.Id, categoria.Id },
              cancellationToken: stoppingToken
          );

          // Si no se encuentra (FindAsync devuelve null), es un registro nuevo.
          if (registroDb == null)
          {
            // Creamos una nueva instancia de la entidad.
            registroDb = new EstadoRecuentoGeneral
            {
              AmbitoGeograficoId = provincia.Id,
              CategoriaId = categoria.Id // Asignamos ambas partes de la clave primaria.
            };
            // Y la añadimos al ChangeTracker de EF para que la inserte en la BD.
            dbContext.EstadosRecuentosGenerales.Add(registroDb);
          }

          // Mapeamos los datos del DTO de la API a nuestra entidad de base de datos.
          // Esto se hace tanto para registros nuevos como para los existentes que se van a actualizar.
          registroDb.MesasEsperadas = estadoDto.MesasEsperadas;
          registroDb.MesasTotalizadas = estadoDto.MesasTotalizadas;
          registroDb.MesasTotalizadasPorcentaje = estadoDto.MesasTotalizadasPorcentaje;
          registroDb.CantidadElectores = estadoDto.CantidadElectores;
          registroDb.CantidadVotantes = estadoDto.CantidadVotantes;
          registroDb.ParticipacionPorcentaje = estadoDto.ParticipacionPorcentaje;
        }
      }

      // PASO 5: Guardar todos los cambios en la base de datos.
      // Al llamar a SaveChangesAsync UNA SOLA VEZ fuera del bucle, EF Core agrupa
      // todas las inserciones y actualizaciones en una única transacción eficiente.
      await dbContext.SaveChangesAsync(stoppingToken);
      _logger.LogInformation("Sondeo de Estado Recuento General completado para todas las categorías.");
    }
    catch (Exception ex)
    {
      // Capturamos cualquier excepción inesperada para que no detenga el worker y la registramos.
      _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Estado Recuento General.");
    }
  }

  /// <summary>
  /// Descarga y sincroniza los catálogos base (Categorías, Ámbitos, Agrupaciones)
  /// desde la API a la base de datos local. Se ejecuta una sola vez al iniciar el worker.
  /// Utiliza una estrategia de guardado en lotes para manejar grandes volúmenes de datos
  /// sin sobrecargar la base de datos.
  /// </summary>
  /// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
  private async Task SincronizarCatalogosMaestrosAsync(CancellationToken stoppingToken)
  {
    try
    {
      _logger.LogInformation("Iniciando sincronización de catálogos maestros...");

      // --- CORRECCIÓN: Usar el _tokenService inyectado ---
      var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken);

      if (string.IsNullOrEmpty(authToken) || stoppingToken.IsCancellationRequested)
      {
        _logger.LogError("No se pudo obtener token para la sincronización de catálogos. La operación se cancela.");
        return;
      }

      // Creamos un scope de servicios para obtener una instancia fresca de DbContext.
      using var scope = _serviceProvider.CreateScope();
      var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();

      // PASO 2: Sincronizar las categorías electorales.
      // Es un catálogo pequeño y es la base para las siguientes consultas.
      var categoriasApi = await _apiService.GetCategoriasAsync(authToken);
      if (categoriasApi is null || !categoriasApi.Any())
      {
        _logger.LogWarning("La API no devolvió datos para el catálogo de Categorías. La sincronización no puede continuar.");
        return;
      }

      var distinctCategorias = categoriasApi.GroupBy(c => c.CategoriaId).Select(g => g.First()).OrderBy(c => c.Orden).ToList();
      _logger.LogInformation("Se procesarán {count} categorías electorales.", distinctCategorias.Count);

      var categoriasEnDb = await dbContext.CategoriasElectorales.ToDictionaryAsync(c => c.Id, c => c, stoppingToken);
      foreach (var categoriaDto in distinctCategorias)
      {
        if (!categoriasEnDb.ContainsKey(categoriaDto.CategoriaId))
        {
          dbContext.CategoriasElectorales.Add(new CategoriaElectoral { Id = categoriaDto.CategoriaId, Nombre = categoriaDto.Nombre, Orden = categoriaDto.Orden });
        }
      }
      // Guardamos las categorías primero para asegurar su existencia.
      await dbContext.SaveChangesAsync(stoppingToken);

      // PASO 3: Cargar los catálogos existentes en memoria para una comparación eficiente.
      // Esto evita hacer miles de consultas a la BD dentro de un bucle.

      // Para los ámbitos, creamos una clave única robusta que funciona incluso con campos nulos.
      var ambitosEnDb = new Dictionary<string, AmbitoGeografico>();
      var todosLosAmbitos = await dbContext.AmbitosGeograficos.ToListAsync(stoppingToken);
      foreach (var ambito in todosLosAmbitos)
      {
        string clave = $"{ambito.NivelId}|{ambito.DistritoId}|{ambito.SeccionProvincialId}|{ambito.SeccionId}|{ambito.MunicipioId}|{ambito.CircuitoId}|{ambito.EstablecimientoId}|{ambito.MesaId}";
        if (!ambitosEnDb.ContainsKey(clave))
        {
          ambitosEnDb.Add(clave, ambito);
        }
      }

      var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);

      // Variable para llevar la cuenta del total de registros insertados.
      int totalCambiosGuardados = 0;

      // PASO 4: Iterar sobre cada categoría para sincronizar sus ámbitos y agrupaciones.
      foreach (var categoria in distinctCategorias)
      {
        if (stoppingToken.IsCancellationRequested) break;
        _logger.LogInformation("--- Sincronizando datos para la categoría: {Nombre} (ID: {Id}) ---", categoria.Nombre, categoria.CategoriaId);

        var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId);
        if (catalogoDto != null)
        {
          // 4.1 - Procesar y añadir ÁMBITOS nuevos al DbContext
          foreach (var ambitoDto in catalogoDto.Ambitos)
          {
            string claveUnica = $"{ambitoDto.NivelId}|{ambitoDto.CodigoAmbitos.DistritoId}|{ambitoDto.CodigoAmbitos.SeccionProvincialId}|{ambitoDto.CodigoAmbitos.SeccionId}|{ambitoDto.CodigoAmbitos.MunicipioId}|{ambitoDto.CodigoAmbitos.CircuitoId}|{ambitoDto.CodigoAmbitos.EstablecimientoId}|{ambitoDto.CodigoAmbitos.MesaId}";

            if (!ambitosEnDb.ContainsKey(claveUnica))
            {
              var nuevoAmbito = new AmbitoGeografico
              {
                Nombre = ambitoDto.Nombre,
                NivelId = ambitoDto.NivelId,
                DistritoId = ambitoDto.CodigoAmbitos.DistritoId,
                SeccionProvincialId = ambitoDto.CodigoAmbitos.SeccionProvincialId,
                SeccionId = ambitoDto.CodigoAmbitos.SeccionId,
                MunicipioId = ambitoDto.CodigoAmbitos.MunicipioId,
                CircuitoId = ambitoDto.CodigoAmbitos.CircuitoId,
                EstablecimientoId = ambitoDto.CodigoAmbitos.EstablecimientoId,
                MesaId = ambitoDto.CodigoAmbitos.MesaId,
              };
              dbContext.AmbitosGeograficos.Add(nuevoAmbito);
              ambitosEnDb.Add(claveUnica, nuevoAmbito); // Añadir también al diccionario en memoria
            }
          }

          // 4.2 - Procesar y añadir AGRUPACIONES nuevas al DbContext
          var provincia = catalogoDto.Ambitos.FirstOrDefault(a => a.NivelId == 10);
          if (provincia != null && !string.IsNullOrEmpty(provincia.CodigoAmbitos.DistritoId))
          {
            // Usamos un try-catch porque no todas las categorías tienen agrupaciones a nivel provincial.
            try
            {
              var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, provincia.CodigoAmbitos.DistritoId, categoria.CategoriaId);
              if (agrupacionesApi != null && agrupacionesApi.Any())
              {
                foreach (var agrupacionDto in agrupacionesApi)
                {
                  if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion))
                  {
                    var nuevaAgrupacion = new AgrupacionPolitica
                    {
                      Id = agrupacionDto.IdAgrupacion,
                      IdTelegrama = agrupacionDto.IdAgrupacionTelegrama,
                      Nombre = agrupacionDto.NombreAgrupacion
                    };
                    dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion);
                    agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion);
                  }
                }
              }
            }
            catch (Exception ex)
            {
              _logger.LogWarning(ex, "No se pudieron obtener agrupaciones para la categoría '{catNombre}' ({catId}).", categoria.Nombre, categoria.CategoriaId);
            }
          }
        }

        // Después de procesar todos los ámbitos y agrupaciones de UNA categoría, guardamos los cambios.
        // Esto divide la inserción masiva de ~50,000 registros en 3 transacciones más pequeñas,
        // evitando timeouts y fallos en la base de datos.
        if (dbContext.ChangeTracker.HasChanges())
        {
          int cambiosEnLote = await dbContext.SaveChangesAsync(stoppingToken);
          totalCambiosGuardados += cambiosEnLote;
          _logger.LogInformation("Guardados {count} registros de catálogo para la categoría '{catNombre}'.", cambiosEnLote, categoria.Nombre);
        }
      }

      // Ya no hay un SaveChangesAsync() gigante aquí.
      _logger.LogInformation("{count} nuevos registros de catálogo han sido guardados en total.", totalCambiosGuardados);
      _logger.LogInformation("Sincronización de catálogos maestros finalizada.");
    }
    catch (Exception ex)
    {
      _logger.LogError(ex, "Ocurrió un error CRÍTICO durante la sincronización de catálogos.");
    }
  }

  /// <summary>
  /// Sondea la proyección de bancas a nivel Provincial y por Sección Electoral.
  /// Esta versión es completamente robusta: maneja respuestas de API vacías o con fechas mal formadas,
  /// guarda la CategoriaId y usa una transacción atómica para la escritura en base de datos.
  /// </summary>
  /// <param name="authToken">El token de autenticación válido para la sesión.</param>
  /// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
  private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken)
  {
    try
    {
      using var scope = _serviceProvider.CreateScope();
      var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();

      var categoriasDeBancas = await dbContext.CategoriasElectorales
          .AsNoTracking()
          .Where(c => c.Nombre.Contains("SENADORES") || c.Nombre.Contains("DIPUTADOS"))
          .ToListAsync(stoppingToken);

      var provincia = await dbContext.AmbitosGeograficos
          .AsNoTracking()
          .FirstOrDefaultAsync(a => a.NivelId == 10, stoppingToken);

      var seccionesElectorales = await dbContext.AmbitosGeograficos
          .AsNoTracking()
          .Where(a => a.NivelId == 20 && a.DistritoId != null && a.SeccionProvincialId != null)
          .ToListAsync(stoppingToken);

      if (!categoriasDeBancas.Any() || provincia == null)
      {
        _logger.LogWarning("No se encontraron categorías de bancas o el ámbito provincial en la BD. Omitiendo sondeo de bancas.");
        return;
      }

      _logger.LogInformation("Iniciando sondeo de Bancas a nivel Provincial y para {count} Secciones Electorales...", seccionesElectorales.Count);

      var todasLasProyecciones = new List<ProyeccionBanca>();
      bool hasReceivedAnyNewData = false;

      // Bucle para el nivel Provincial
      foreach (var categoria in categoriasDeBancas)
      {
        if (stoppingToken.IsCancellationRequested) break;
        var repartoBancasDto = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, categoria.Id);

        if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas)
        {
          hasReceivedAnyNewData = true;

          // --- SEGURIDAD: Usar TryParse para la fecha ---
          DateTime fechaTotalizacion;
          if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate))
          {
            // Si la fecha es inválida (nula, vacía, mal formada), lo registramos y usamos la hora actual como respaldo.
            _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas provinciales. Usando la hora actual.", repartoBancasDto.FechaTotalizacion);
            fechaTotalizacion = DateTime.UtcNow;
          }
          else
          {
            fechaTotalizacion = parsedDate.ToUniversalTime();
          }

          foreach (var banca in bancas)
          {
            todasLasProyecciones.Add(new ProyeccionBanca
            {
              AmbitoGeograficoId = provincia.Id,
              AgrupacionPoliticaId = banca.IdAgrupacion,
              NroBancas = banca.NroBancas,
              CategoriaId = categoria.Id,
              FechaTotalizacion = fechaTotalizacion
            });
          }
        }
      }

      // Bucle para el nivel de Sección Electoral
      foreach (var seccion in seccionesElectorales)
      {
        if (stoppingToken.IsCancellationRequested) break;
        foreach (var categoria in categoriasDeBancas)
        {
          if (stoppingToken.IsCancellationRequested) break;
          var repartoBancasDto = await _apiService.GetBancasAsync(authToken, seccion.DistritoId!, seccion.SeccionProvincialId!, categoria.Id);

          if (repartoBancasDto?.RepartoBancas is { Count: > 0 } bancas)
          {
            hasReceivedAnyNewData = true;

            // --- APLICAMOS SEGURIDAD AQUÍ ---
            DateTime fechaTotalizacion;
            if (!DateTime.TryParse(repartoBancasDto.FechaTotalizacion, out var parsedDate))
            {
              _logger.LogWarning("No se pudo parsear FechaTotalizacion ('{dateString}') para bancas de sección. Usando la hora actual.", repartoBancasDto.FechaTotalizacion);
              fechaTotalizacion = DateTime.UtcNow;
            }
            else
            {
              fechaTotalizacion = parsedDate.ToUniversalTime();
            }

            foreach (var banca in bancas)
            {
              todasLasProyecciones.Add(new ProyeccionBanca
              {
                AmbitoGeograficoId = seccion.Id,
                AgrupacionPoliticaId = banca.IdAgrupacion,
                NroBancas = banca.NroBancas,
                CategoriaId = categoria.Id,
                FechaTotalizacion = fechaTotalizacion
              });
            }
          }
        }
      }

      if (hasReceivedAnyNewData)
      {
        _logger.LogInformation("Se recibieron datos válidos de bancas. Procediendo a actualizar la base de datos...");
        await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken);

        await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM ProyeccionesBancas", stoppingToken);
        await dbContext.ProyeccionesBancas.AddRangeAsync(todasLasProyecciones, stoppingToken);
        await dbContext.SaveChangesAsync(stoppingToken);
        await transaction.CommitAsync(stoppingToken);

        _logger.LogInformation("La tabla de proyecciones ha sido actualizada con {count} registros.", todasLasProyecciones.Count);
      }
      else
      {
        _logger.LogInformation("Sondeo de Bancas completado. No se encontraron datos nuevos de proyección, la tabla no fue modificada.");
      }
    }
    catch (OperationCanceledException)
    {
      _logger.LogInformation("Sondeo de bancas cancelado.");
    }
    catch (Exception ex)
    {
      _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Bancas.");
    }
  }

  /// <summary>
  /// Busca y descarga nuevos telegramas de forma masiva y concurrente.
  /// Este método crea una lista de todas las combinaciones de Partido/Categoría,
  /// las consulta a la API con un grado de paralelismo controlado, y cada tarea concurrente
  /// maneja su propia lógica de descarga y guardado en la base de datos.
  /// </summary>
  /// <param name="authToken">El token de autenticación válido para la sesión.</param>
  /// <param name="stoppingToken">El token de cancelación para detener la operación.</param>
  private async Task SondearNuevosTelegramasAsync(string authToken, CancellationToken stoppingToken)
  {
    try
    {
      _logger.LogInformation("--- Iniciando sondeo de Nuevos Telegramas (modo de bajo perfil) ---");

      using var scope = _serviceProvider.CreateScope();
      var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();

      // La obtención de partidos y categorías no cambia
      var partidos = await dbContext.AmbitosGeograficos.AsNoTracking()
          .Where(a => a.NivelId == 30 && a.DistritoId != null && a.SeccionId != null)
          .ToListAsync(stoppingToken);
      var categorias = await dbContext.CategoriasElectorales.AsNoTracking().ToListAsync(stoppingToken);

      if (!partidos.Any() || !categorias.Any()) return;

      foreach (var partido in partidos)
      {
        foreach (var categoria in categorias)
        {
          if (stoppingToken.IsCancellationRequested) return;

          var listaTelegramasApi = await _apiService.GetTelegramasTotalizadosAsync(authToken, partido.DistritoId!, partido.SeccionId!, categoria.Id);

          if (listaTelegramasApi is { Count: > 0 })
          {
            // Creamos el DbContext para la operación de guardado
            using var innerScope = _serviceProvider.CreateScope();
            var innerDbContext = innerScope.ServiceProvider.GetRequiredService<EleccionesDbContext>();

            var idsYaEnDb = await innerDbContext.Telegramas
                .Where(t => listaTelegramasApi.Contains(t.Id))
                .Select(t => t.Id).ToListAsync(stoppingToken);

            var nuevosTelegramasIds = listaTelegramasApi.Except(idsYaEnDb).ToList();

            if (nuevosTelegramasIds.Any())
            {
              _logger.LogInformation("Se encontraron {count} telegramas nuevos en '{partido}' para '{cat}'. Descargando...", nuevosTelegramasIds.Count, partido.Nombre, categoria.Nombre);

              var originalTimeout = innerDbContext.Database.GetCommandTimeout();
              try
              {
                innerDbContext.Database.SetCommandTimeout(180);
                _logger.LogDebug("Timeout de BD aumentado a 180s para descarga de telegramas.");

                int contadorLote = 0;
                const int tamanoLote = 100;

                foreach (var mesaId in nuevosTelegramasIds)
                {
                  if (stoppingToken.IsCancellationRequested) return;

                  var telegramaFile = await _apiService.GetTelegramaFileAsync(authToken, mesaId);
                  if (telegramaFile != null)
                  {
                    var ambitoMesa = await innerDbContext.AmbitosGeograficos.AsNoTracking()
                        .FirstOrDefaultAsync(a => a.MesaId == mesaId, stoppingToken);

                    if (ambitoMesa != null)
                    {
                      var nuevoTelegrama = new Telegrama
                      {
                        Id = telegramaFile.NombreArchivo,
                        AmbitoGeograficoId = ambitoMesa.Id,
                        ContenidoBase64 = telegramaFile.Imagen,
                        FechaEscaneo = DateTime.Parse(telegramaFile.FechaEscaneo).ToUniversalTime(),
                        FechaTotalizacion = DateTime.Parse(telegramaFile.FechaTotalizacion).ToUniversalTime()
                      };
                      await innerDbContext.Telegramas.AddAsync(nuevoTelegrama, stoppingToken);
                      contadorLote++;
                    }
                    else
                    {
                      _logger.LogWarning("No se encontró un ámbito geográfico para la mesa con MesaId {MesaId}. El telegrama no será guardado.", mesaId);
                    }
                  }
                  await Task.Delay(250, stoppingToken);

                  if (contadorLote >= tamanoLote)
                  {
                    await innerDbContext.SaveChangesAsync(stoppingToken);
                    _logger.LogInformation("Guardado un lote de {count} telegramas.", contadorLote);
                    contadorLote = 0;
                  }
                }

                if (contadorLote > 0)
                {
                  await innerDbContext.SaveChangesAsync(stoppingToken);
                  _logger.LogInformation("Guardado el último lote de {count} telegramas.", contadorLote);
                }
              }
              finally
              {
                innerDbContext.Database.SetCommandTimeout(originalTimeout);
                _logger.LogDebug("Timeout de BD restaurado a su valor original ({timeout}s).", originalTimeout);
              }
            } // Fin del if (nuevosTelegramasIds.Any())

            // Movemos el delay aquí para que solo se ejecute si hubo telegramas en la respuesta de la API
            await Task.Delay(100, stoppingToken);
          } // Fin del if (listaTelegramasApi is not null)
        }
      }
      _logger.LogInformation("Sondeo de Telegramas completado.");
    }
    catch (OperationCanceledException)
    {
      _logger.LogInformation("Sondeo de telegramas cancelado.");
    }
    catch (Exception ex)
    {
      _logger.LogError(ex, "Ocurrió un error CRÍTICO en el sondeo de Telegramas.");
    }
  }
}