using EFCDesk.Entidades; using System; using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace EFCDesk.Classes { public class ApiException : Exception { public int StatusCode { get; } public ApiException(string message, int statusCode) : base(message) { StatusCode = statusCode; } } public class ApiClient : IDisposable { private readonly HttpClient _httpClient; private readonly int _maxRetries; private readonly TimeSpan _retryDelay; private string? _currentToken; private string? _currentRefreshToken; private readonly object _tokenLock = new object(); public ApiClient(TimeSpan? timeout = null, int maxRetries = 3, TimeSpan? retryDelay = null) { _httpClient = new HttpClient(); if (timeout.HasValue) _httpClient.Timeout = timeout.Value; else _httpClient.Timeout = TimeSpan.FromSeconds(30); _maxRetries = maxRetries >= 0 ? maxRetries : 3; _retryDelay = retryDelay ?? TimeSpan.FromSeconds(2); } //private void SetAuthorization(string token) //{ // if (string.IsNullOrWhiteSpace(token)) // throw new ArgumentException("El token no puede estar vacío.", nameof(token)); // _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", token); //} public string EndpointLoginToken() { return @"/api/v1/token/"; } public string EndpointRefreshToken() { return @"/api/v1/token/refresh/"; } private void SetAuthorizationBearer(string token, string refresh_token) { if (string.IsNullOrWhiteSpace(token)) throw new ArgumentException("El token no puede estar vacío.", nameof(token)); if (string.IsNullOrWhiteSpace(refresh_token)) throw new ArgumentException("El refresh token no puede estar vacío.", nameof(refresh_token)); lock (_tokenLock) { _currentToken = token; _currentRefreshToken = refresh_token; _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); } } private async Task TryRefreshTokenAsync() { try { Globales.configJson = ConfiguracionJSON.LoadFromJson(); // Obtener el refresh token actual guardado string? savedToken = Globales.refresToken; if (string.IsNullOrEmpty(savedToken)) return false; // Usar el endpoint de refresh token string dominio = Globales.configJson.DominioExp ?? ""; if (string.IsNullOrEmpty(dominio)) return false; string refreshUrl = dominio + EndpointRefreshToken(); // Crear solicitud de refresh con el token actual var refreshData = new { refresh = savedToken }; var jsonContent = JsonSerializer.Serialize(refreshData); using var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json"); // Limpiar headers para esta solicitud _httpClient.DefaultRequestHeaders.Authorization = null; var response = await _httpClient.PostAsync(refreshUrl, content); if (response.IsSuccessStatusCode) { string responseContent = await response.Content.ReadAsStringAsync(); if (IsJson(responseContent) && HasJsonData(responseContent)) { using var doc = JsonDocument.Parse(responseContent); var root = doc.RootElement; // Buscar nuevo token en la respuesta (ajustar según tu API) string newToken = ""; if (root.TryGetProperty("access", out var accessProp)) { newToken = accessProp.GetString() ?? ""; } else if (root.TryGetProperty("token", out var tokenProp)) { newToken = tokenProp.GetString() ?? ""; } // Buscar nuevo refresh token en la respuesta (ajustar según tu API) string newRefreshToken = ""; if (root.TryGetProperty("refresh", out var refreshProp)) { newRefreshToken = refreshProp.GetString() ?? ""; } else if (root.TryGetProperty("refresh_token", out var refreshtokenProp)) { newRefreshToken = refreshtokenProp.GetString() ?? ""; } if (!string.IsNullOrEmpty(newToken) && !string.IsNullOrEmpty(newRefreshToken)) { // Guardar nuevo token Globales.accesToken = newToken; // Guardar nuevo refresh token Globales.refresToken = newRefreshToken; // Actualizar headers SetAuthorizationBearer(newToken, newRefreshToken); return true; } } } return false; } catch (Exception ex) { Globales.logger.LogError("Error al intentar refresh token", ex); return false; } finally { // Restaurar el token actual si el refresh falló if (!string.IsNullOrEmpty(_currentToken) && !string.IsNullOrEmpty(_currentRefreshToken)) { SetAuthorizationBearer(_currentToken, _currentRefreshToken); } } } private async Task EnsureTokenAsync() { // Si ya tenemos token en memoria, usarlo if (!string.IsNullOrEmpty(_currentToken) && !string.IsNullOrEmpty(_currentRefreshToken)) { SetAuthorizationBearer(_currentToken, _currentRefreshToken); return; } // Intentar obtener token guardado string? savedToken = Globales.accesToken; string? savedRefreshToken = Globales.refresToken; if (!string.IsNullOrEmpty(savedToken) && !string.IsNullOrEmpty(savedRefreshToken)) { SetAuthorizationBearer(savedToken, savedRefreshToken); return; } throw new InvalidOperationException("No hay token disponible. Inicie sesión primero."); } private async Task SendWithRetriesAsync(Func> sendFunc, bool requireAuth = true) { int attempts = 0; while (true) { attempts++; try { // Asegurar que tenemos token antes de enviar (solo si requiere auth) if (requireAuth) await EnsureTokenAsync(); var response = await sendFunc(); if (!response.IsSuccessStatusCode) { // Si es 401 Unauthorized y requiere auth, intentar refresh token if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && requireAuth) { bool refreshed = await TryRefreshTokenAsync(); if (refreshed) { // Reintentar con nuevo token response = await sendFunc(); if (response.IsSuccessStatusCode) return await response.Content.ReadAsStringAsync(); } } string errorContent = await response.Content.ReadAsStringAsync(); // throw new ApiException($"Error {response.StatusCode}: {errorContent}", (int)response.StatusCode); return errorContent; } return await response.Content.ReadAsStringAsync(); } catch (TaskCanceledException) when (attempts <= _maxRetries) { // Timeout: reintentar await Task.Delay(_retryDelay); } catch (HttpRequestException) when (attempts <= _maxRetries) { // Error red: reintentar await Task.Delay(_retryDelay); } catch (TaskCanceledException) { return CrearJsonError(408, "Tiempo de espera agotado"); } catch (HttpRequestException ex) { return CrearJsonError(503, $"Error de red: {ex.Message}"); } catch (Exception ex) { return CrearJsonError(500, $"Error inesperado: {ex.Message}"); } } } public async Task GetAsync(string url) { return await SendWithRetriesAsync(() => _httpClient.GetAsync(url)); } public async Task PostJsonAsync(string url, string jsonContent) { using var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json"); return await SendWithRetriesAsync(() => _httpClient.PostAsync(url, content)); } public async Task PutJsonAsync(string url, string jsonContent) { using var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json"); return await SendWithRetriesAsync(() => _httpClient.PutAsync(url, content)); } public async Task DeleteAsync(string url) { return await SendWithRetriesAsync(() => _httpClient.DeleteAsync(url)); } // Método para enviar archivo y JSON juntos en multipart/form-data public async Task PostMultipartAsync(string url, string filePath, object jsonData, string fileFieldName = "archivo", string jsonFieldName = "data") { if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) throw new ArgumentException("El archivo no existe o la ruta es inválida.", nameof(filePath)); using var form = new MultipartFormDataContent(); // Archivo var fileStream = File.OpenRead(filePath); var fileContent = new StreamContent(fileStream); form.Add(fileContent, fileFieldName, Path.GetFileName(filePath)); // JSON var jsonString = JsonSerializer.Serialize(jsonData); var jsonContent = new StringContent(jsonString, System.Text.Encoding.UTF8, "application/json"); form.Add(jsonContent, jsonFieldName); return await SendWithRetriesAsync(() => _httpClient.PostAsync(url, form)); } public async Task PostMultipartAsync(string url, string filePath, string organizacion, string pedimento, string documentType, string fuente, string fileFieldName = "archivo" ) { if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) throw new ArgumentException("El archivo no existe o la ruta es inválida.", nameof(filePath)); using var form = new MultipartFormDataContent(); // Archivo var fileStream = File.OpenRead(filePath); var fileContent = new StreamContent(fileStream); fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); form.Add(fileContent, fileFieldName, Path.GetFileName(filePath)); // Campos de texto form.Add(new StringContent(organizacion), "organizacion"); form.Add(new StringContent(pedimento), "pedimento"); form.Add(new StringContent(documentType), "document_type"); form.Add(new StringContent(fuente), "fuente"); return await SendWithRetriesAsync(() => _httpClient.PostAsync(url, form)); } //public async Task PostMultipartZipAsync(string url, string token, byte[] zipBytes, string zipFileName, string fileFieldName = "archivos") //{ // if (zipBytes == null || zipBytes.Length == 0) // throw new ArgumentException("El ZIP está vacío o no es válido.", nameof(zipBytes)); // SetAuthorization(token); // using var form = new MultipartFormDataContent(); // // Convertimos el byte[] a contenido del archivo // var fileContent = new ByteArrayContent(zipBytes); // fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/zip"); // // Nombre del archivo en el multipart // form.Add(fileContent, fileFieldName, zipFileName); // // Campos de texto // //form.Add(new StringContent(pedimento), "pedimento"); // return await SendWithRetriesAsync(() => _httpClient.PostAsync(url, form)); //} public async Task PostMultipartZipAsync(string url, string filePath, string zipFileName, Dictionary metadatos, string fileFieldName = "archivos") { if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) throw new ArgumentException("El archivo no existe o la ruta es inválida.", nameof(filePath)); using var form = new MultipartFormDataContent(); // Archivo var fileStream = File.OpenRead(filePath); var fileContent = new StreamContent(fileStream); fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); form.Add(fileContent, fileFieldName, Path.GetFileName(filePath)); if (metadatos != null) { foreach (var kvp in metadatos) { form.Add(new StringContent(kvp.Value), kvp.Key); } } // Campos de texto //form.Add(new StringContent(organizacion), "organizacion"); //form.Add(new StringContent(pedimento), "pedimento"); //form.Add(new StringContent(documentType), "document_type"); //form.Add(new StringContent(fuente), "fuente"); return await SendWithRetriesAsync(() => _httpClient.PostAsync(url, form)); } // Método existente que acepta string JSON public async Task PostJsonWithoutAuthAsync(string url, string jsonContent) { using var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json"); return await SendWithRetriesAsync(() => _httpClient.PostAsync(url, content), requireAuth: false); } // Nueva sobrecarga que acepta cualquier objeto public async Task PostJsonWithoutAuthAsync(string url, object data) { var jsonContent = JsonSerializer.Serialize(data); return await PostJsonWithoutAuthAsync(url, jsonContent); } public bool IsJson(string response) { if (string.IsNullOrWhiteSpace(response)) return false; try { using var doc = JsonDocument.Parse(response); return true; } catch (JsonException) { return false; } } public bool HasJsonData(string response) { if (string.IsNullOrWhiteSpace(response)) return false; try { using var doc = JsonDocument.Parse(response); var root = doc.RootElement; if (root.ValueKind == JsonValueKind.Object) { return root.EnumerateObject().Any(); } else if (root.ValueKind == JsonValueKind.Array) { return root.GetArrayLength() > 0; } else { return false; } } catch (JsonException) { return false; } } // Método para deserializar el JSON a un objeto genérico public T? TryParseJson(string response) where T : class { if (string.IsNullOrWhiteSpace(response)) return null; try { return JsonSerializer.Deserialize(response); } catch (JsonException ex) { Globales.logger.LogError($"Error al deserializar JSON: {ex.Message}"); return null; } } private static string CrearJsonError(int status, string message) { return $$""" { "success": false, "status": {{status}}, "message": "{{message.Replace("\"", "\\\"")}}" } """; } public void Dispose() { _httpClient.Dispose(); } } }