// src/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 const int EleccionId = 2;
  private readonly ILogger<LowPriorityDataWorker> _logger;
  private readonly SharedTokenService _tokenService;
  private readonly IServiceProvider _serviceProvider;
  private readonly IElectoralApiService _apiService;
  private readonly WorkerConfigService _configService;

  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 (Proyecciones) iniciado.");

    // Sincronización inicial de catálogos (solo se ejecuta una vez)
    await SincronizarCatalogosMaestrosAsync(stoppingToken);

    while (!stoppingToken.IsCancellationRequested)
    {
      var settings = await _configService.GetSettingsAsync();
      if (!settings.BajasActivado)
      {
        _logger.LogInformation("El sondeo de proyecciones está desactivado. Esperando 5 minutos.");
        await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        continue;
      }

      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;
      }

      _logger.LogInformation("--- Iniciando Ciclo de Sondeo de Proyección de Bancas ---");
      await SondearProyeccionBancasAsync(authToken, stoppingToken);
      _logger.LogInformation("--- Ciclo de Sondeo de Proyección de Bancas completado. ---");

      try
      {
        await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
      }
      catch (TaskCanceledException)
      {
        break;
      }
    }
  }

  /// <summary>
  /// Sondea la proyección de bancas a nivel Provincial.
  /// Itera sobre cada provincia y consulta las categorías específicas
  /// (Diputados para todas, Senadores solo para las que renuevan).
  /// </summary>
  private async Task SondearProyeccionBancasAsync(string authToken, CancellationToken stoppingToken)
  {
    try
    {
      using var scope = _serviceProvider.CreateScope();
      var dbContext = scope.ServiceProvider.GetRequiredService<EleccionesDbContext>();

      // 1. Definimos las constantes y la lista de provincias que renuevan senadores.
      const int catDiputadosNac = 3;
      const int catSenadoresNac = 2;
      var provinciasQueRenuevanSenadores = new HashSet<string> { "01", "06", "08", "15", "16", "17", "22", "24" };

      // 2. Obtenemos las provincias (ámbitos de Nivel 10) de nuestra base de datos.
      var ambitosProvinciales = await dbContext.AmbitosGeograficos
          .AsNoTracking()
          .Where(a => a.NivelId == 10 && a.DistritoId != null)
          .ToListAsync(stoppingToken);

      if (!ambitosProvinciales.Any())
      {
        _logger.LogWarning("No se encontraron ámbitos provinciales en la BD. Omitiendo sondeo de bancas.");
        return;
      }

      _logger.LogInformation("Iniciando sondeo de Bancas a nivel Provincial para {count} provincias...", ambitosProvinciales.Count);

      var todasLasProyecciones = new List<ProyeccionBanca>();
      bool hasReceivedAnyNewData = false;
      var agrupacionesEnDb = await dbContext.AgrupacionesPoliticas.ToDictionaryAsync(a => a.Id, a => a, stoppingToken);

      // 3. Iteramos sobre CADA provincia para hacer las llamadas específicas.
      foreach (var provincia in ambitosProvinciales)
      {
        if (stoppingToken.IsCancellationRequested) break;

        // Función auxiliar para no repetir código. Procesa la respuesta de la API.
        async Task ProcesarRespuestaApi(Core.DTOs.RepartoBancasDto? dto, int categoriaId)
        {
          if (dto?.RepartoBancas is not { Count: > 0 } bancas) return;

          hasReceivedAnyNewData = true;
          DateTime fechaTotalizacion = DateTime.TryParse(dto.FechaTotalizacion, out var parsedDate) ? parsedDate : DateTime.UtcNow;

          foreach (var banca in bancas)
          {
            if (!agrupacionesEnDb.ContainsKey(banca.IdAgrupacion))
            {
              var nuevaAgrupacion = new AgrupacionPolitica
              {
                Id = banca.IdAgrupacion,
                Nombre = banca.NombreAgrupacion,
                IdTelegrama = banca.IdAgrupacionTelegrama ?? string.Empty
              };
              await dbContext.AgrupacionesPoliticas.AddAsync(nuevaAgrupacion, stoppingToken);
              agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion);
            }
            todasLasProyecciones.Add(new ProyeccionBanca
            {
              EleccionId = EleccionId,
              AmbitoGeograficoId = provincia.Id,
              AgrupacionPoliticaId = banca.IdAgrupacion,
              NroBancas = banca.NroBancas,
              CategoriaId = categoriaId, // Usamos la categoría correcta
              FechaTotalizacion = fechaTotalizacion
            });
          }
        }

        // 4. Siempre consultamos por Diputados (Categoría 3) para la provincia actual.
        //    Pasamos 'null' en seccionProvincialId para obtener el total provincial.
        var diputadosDto = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, catDiputadosNac);
        await ProcesarRespuestaApi(diputadosDto, catDiputadosNac);

        if (stoppingToken.IsCancellationRequested) break;

        // 5. SOLO si la provincia está en la lista, consultamos por Senadores (Categoría 2).
        if (provinciasQueRenuevanSenadores.Contains(provincia.DistritoId!))
        {
          var senadoresDto = await _apiService.GetBancasAsync(authToken, provincia.DistritoId!, null, catSenadoresNac);
          await ProcesarRespuestaApi(senadoresDto, catSenadoresNac);
        }
      }

      // La lógica para guardar en la base de datos se mantiene, ya que es robusta.
      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);

        if (dbContext.ChangeTracker.HasChanges())
        {
          await dbContext.SaveChangesAsync(stoppingToken);
        }

        await dbContext.ProyeccionesBancas
            .Where(p => p.EleccionId == EleccionId && (p.CategoriaId == catDiputadosNac || p.CategoriaId == catSenadoresNac))
            .ExecuteDeleteAsync(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, 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>
  /// 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.
  /// La sincronización de agrupaciones ahora itera por cada provincia para obtener listas completas.
  /// </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...");
      var authToken = await _tokenService.GetValidAuthTokenAsync(stoppingToken);
      if (string.IsNullOrEmpty(authToken))
      {
        _logger.LogError("No se pudo obtener token para la sincronización de catálogos.");
        return;
      }

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

      // --- PASO 1: SINCRONIZAR CATEGORÍAS ---
      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.");
        return;
      }
      var distinctCategorias = categoriasApi.GroupBy(c => c.CategoriaId).Select(g => g.First()).OrderBy(c => c.Orden).ToList();
      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 });
        }
      }
      await dbContext.SaveChangesAsync(stoppingToken);
      _logger.LogInformation("Catálogo de Categorías Electorales sincronizado.");

      // --- PASO 2: SINCRONIZAR ÁMBITOS GEOGRÁFICOS ---
      _logger.LogInformation("Iniciando sincronización de Ámbitos Geográficos...");
      var ambitosEnDbKeys = new HashSet<string>(
          await dbContext.AmbitosGeograficos.Select(a => $"{a.NivelId}|{a.DistritoId}|{a.SeccionProvincialId}|{a.SeccionId}|{a.MunicipioId}|{a.CircuitoId}|{a.EstablecimientoId}|{a.MesaId}").ToListAsync(stoppingToken)
      );
      int totalNuevosAmbitos = 0;

      foreach (var categoria in distinctCategorias)
      {
        if (stoppingToken.IsCancellationRequested) break;
        var catalogoDto = await _apiService.GetCatalogoAmbitosAsync(authToken, categoria.CategoriaId);
        if (catalogoDto != null)
        {
          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 (ambitosEnDbKeys.Add(claveUnica))
            {
              string nombreCorregido = ambitoDto.Nombre;
              if (ambitoDto.CodigoAmbitos.DistritoId == "01" && ambitoDto.NivelId == 30 && int.TryParse(ambitoDto.Nombre, out int numeroComuna))
              {
                nombreCorregido = $"COMUNA {numeroComuna}";
              }
              dbContext.AmbitosGeograficos.Add(new AmbitoGeografico
              {
                Nombre = nombreCorregido,
                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,
              });
            }
          }
        }
        if (dbContext.ChangeTracker.HasChanges())
        {
          int ambitosGuardados = await dbContext.SaveChangesAsync(stoppingToken);
          totalNuevosAmbitos += ambitosGuardados;
        }
      }
      _logger.LogInformation("Catálogo de Ámbitos Geográficos sincronizado. Se guardaron {count} nuevos registros en total.", totalNuevosAmbitos);

      _logger.LogInformation("Iniciando sincronización de Agrupaciones Políticas por provincia y categoría...");

      // Obtenemos todas las provincias (Nivel 10) que ya sincronizamos.
      var provincias = await dbContext.AmbitosGeograficos
          .AsNoTracking()
          .Where(a => a.NivelId == 10 && a.DistritoId != null)
          .ToListAsync(stoppingToken);

      // Definimos las constantes para las categorías y las provincias que renuevan senadores.
      const int catDiputadosNac = 3;
      const int catSenadoresNac = 2;
      var provinciasQueRenuevanSenadores = new HashSet<string> { "01", "06", "08", "15", "16", "17", "22", "24" };

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

      // Iteramos sobre cada provincia.
      foreach (var provincia in provincias)
      {
        if (stoppingToken.IsCancellationRequested) break;

        // Determinamos qué categorías consultar para esta provincia.
        var categoriasParaSondear = new List<int> { catDiputadosNac }; // Todas las provincias eligen diputados.
        if (provinciasQueRenuevanSenadores.Contains(provincia.DistritoId!))
        {
          categoriasParaSondear.Add(catSenadoresNac); // Solo algunas eligen senadores.
        }

        // Iteramos sobre las categorías aplicables para la provincia actual.
        foreach (var categoriaId in categoriasParaSondear)
        {
          _logger.LogDebug("Consultando agrupaciones para {Provincia} ({DistritoId}), Categoría: {CategoriaId}", provincia.Nombre, provincia.DistritoId, categoriaId);
          var agrupacionesApi = await _apiService.GetAgrupacionesAsync(authToken, provincia.DistritoId, categoriaId);

          if (agrupacionesApi != null)
          {
            foreach (var agrupacionDto in agrupacionesApi)
            {
              // Si la agrupación no está en nuestro diccionario, la añadimos.
              if (!agrupacionesEnDb.ContainsKey(agrupacionDto.IdAgrupacion))
              {
                var nuevaAgrupacion = new AgrupacionPolitica
                {
                  Id = agrupacionDto.IdAgrupacion,
                  IdTelegrama = agrupacionDto.IdAgrupacionTelegrama,
                  Nombre = agrupacionDto.NombreAgrupacion
                };
                dbContext.AgrupacionesPoliticas.Add(nuevaAgrupacion);
                // La añadimos también al diccionario para no intentar agregarla de nuevo en este ciclo.
                agrupacionesEnDb.Add(nuevaAgrupacion.Id, nuevaAgrupacion);
              }
            }
          }
        }
      }
      // Guardamos todos los cambios de agrupaciones al final, en una sola transacción.
      int agrupacionesGuardadas = await dbContext.SaveChangesAsync(stoppingToken);
      _logger.LogInformation("Catálogo de Agrupaciones Políticas sincronizado. Se guardaron {count} nuevos registros.", agrupacionesGuardadas);

      _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.");
    }
  }
}