using AngleSharp; using Mercados.Core.Entities; using Mercados.Infrastructure.Persistence.Repositories; using Microsoft.Extensions.Logging; using System.Globalization; namespace Mercados.Infrastructure.DataFetchers { public class MercadoAgroFetcher : IDataFetcher { public string SourceName => "MercadoAgroganadero"; private const string DataUrl = "https://www.mercadoagroganadero.com.ar/dll/hacienda6.dll/haciinfo000225"; private readonly IHttpClientFactory _httpClientFactory; private readonly ICotizacionGanadoRepository _cotizacionRepository; private readonly IFuenteDatoRepository _fuenteDatoRepository; private readonly ILogger _logger; public MercadoAgroFetcher( IHttpClientFactory httpClientFactory, ICotizacionGanadoRepository cotizacionRepository, IFuenteDatoRepository fuenteDatoRepository, ILogger logger) { _httpClientFactory = httpClientFactory; _cotizacionRepository = cotizacionRepository; _fuenteDatoRepository = fuenteDatoRepository; _logger = logger; } public async Task<(bool Success, string Message)> FetchDataAsync() { _logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName); try { var htmlContent = await GetHtmlContentAsync(); if (string.IsNullOrEmpty(htmlContent)) { return (false, "No se pudo obtener el contenido HTML."); } var cotizaciones = ParseHtmlToEntities(htmlContent); if (!cotizaciones.Any()) { return (false, "No se encontraron cotizaciones válidas en el HTML."); } await _cotizacionRepository.GuardarMuchosAsync(cotizaciones); await UpdateSourceInfoAsync(); _logger.LogInformation("Fetch para {SourceName} completado exitosamente. Se guardaron {Count} registros.", SourceName, cotizaciones.Count); return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros."); } catch (Exception ex) { _logger.LogError(ex, "Ocurrió un error durante el fetch para {SourceName}.", SourceName); return (false, $"Error: {ex.Message}"); } } private async Task GetHtmlContentAsync() { // Pedimos el cliente HTTP con el nombre específico que tiene la política de Polly var client = _httpClientFactory.CreateClient("MercadoAgroFetcher"); // Es importante simular un navegador para evitar bloqueos. client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"); var response = await client.GetAsync(DataUrl); response.EnsureSuccessStatusCode(); // El sitio usa una codificación específica, hay que decodificarla correctamente. var stream = await response.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream, System.Text.Encoding.GetEncoding("windows-1252")); return await reader.ReadToEndAsync(); } private List ParseHtmlToEntities(string html) { var config = Configuration.Default; var context = BrowsingContext.New(config); var document = context.OpenAsync(req => req.Content(html)).Result; var tabla = document.QuerySelector("table.table.table-striped"); if (tabla == null) { _logger.LogWarning("No se encontró la tabla de cotizaciones en el HTML."); return new List(); } var cotizaciones = new List(); // Omitimos las primeras 2 filas (cabeceras) y las últimas 2 (totales/pies). var filas = tabla.QuerySelectorAll("tr").Skip(2).SkipLast(2); foreach (var fila in filas) { var celdas = fila.QuerySelectorAll("td").Select(c => c.TextContent.Trim()).ToList(); if (celdas.Count < 12) continue; // Fila inválida try { var cotizacion = new CotizacionGanado { Categoria = celdas[1], Especificaciones = $"{celdas[2]} - {celdas[3]}", Maximo = ParseDecimal(celdas[4]), Minimo = ParseDecimal(celdas[5]), Promedio = ParseDecimal(celdas[6]), Mediano = ParseDecimal(celdas[7]), Cabezas = ParseInt(celdas[8]), KilosTotales = ParseInt(celdas[9]), KilosPorCabeza = ParseInt(celdas[10]), ImporteTotal = ParseDecimal(celdas[11]), FechaRegistro = DateTime.UtcNow }; cotizaciones.Add(cotizacion); } catch (Exception ex) { _logger.LogWarning(ex, "No se pudo parsear una fila de la tabla. Contenido: {RowContent}", string.Join(" | ", celdas)); } } return cotizaciones; } private async Task UpdateSourceInfoAsync() { var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName); if (fuente == null) { await _fuenteDatoRepository.CrearAsync(new FuenteDato { Nombre = SourceName, Url = DataUrl, UltimaEjecucionExitosa = DateTime.UtcNow }); } else { fuente.Url = DataUrl; fuente.UltimaEjecucionExitosa = DateTime.UtcNow; await _fuenteDatoRepository.ActualizarAsync(fuente); } } // --- Funciones de Ayuda para Parseo --- private readonly CultureInfo _cultureInfo = new CultureInfo("es-AR"); private decimal ParseDecimal(string value) { // El sitio usa '.' como separador de miles y ',' como decimal. // Primero quitamos el de miles, luego reemplazamos la coma decimal por un punto. var cleanValue = value.Replace("$", "").Replace(".", "").Trim(); return decimal.Parse(cleanValue, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, _cultureInfo); } private int ParseInt(string value) { return int.Parse(value.Replace(".", ""), CultureInfo.InvariantCulture); } } }