package com.banesco.common.application.service; import com.banesco.common.application.usecase.HttpClientUseCase; import com.banesco.common.domain.exception.HttpApiResponseException; import com.banesco.common.domain.exception.HttpStatusCodeException; import com.banesco.common.domain.model.*; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.client.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.extern.slf4j.Slf4j; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @Slf4j @ApplicationScoped public class HttpClientService implements HttpClientUseCase { private final ObjectMapper objectMapper; @Inject public HttpClientService(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public T execute(HttpRequest request) { return executeInternal(request); } @Override public ApiResponse executeApiResponse(HttpRequest request) { return executeInternal(request); } @Override public ApiResponse> executeApiResponseList( HttpRequest request ) { return executeInternal(request); } @Override public ApiPrivateResponse> executeApiPrivateResponse( HttpRequest request ) { return executeInternal(request); } @Override public ApiPrivateResponse, ApiPrivateError>> executeApiPrivateResponseList( HttpRequest request ) { return executeInternal(request); } private T executeInternal(HttpRequest request) { String finalUrl = buildFinalUrl(request); if (request.isLogRequestBody()) { log.info("URL final: {}", finalUrl); if (request.getHeaders() != null && !request.getHeaders().isEmpty()) { log.info("Headers: {}", request.getHeaders()); } if (request.getQueryParams() != null && !request.getQueryParams().isEmpty()) { log.info("Query params: {}", request.getQueryParams()); } if (request.getBody() != null) { log.info("Body: {}", request.getBody()); } } try (Client client = createClient(request.getConnectTimeout(), request.getReadTimeout())) { WebTarget target = client.target(finalUrl); Invocation.Builder builder = target.request(MediaType.APPLICATION_JSON); if (request.getHeaders() != null) { request.getHeaders().forEach(builder::header); } Response response = buildRequest(builder, request); return handleResponse(request, response); } catch (HttpStatusCodeException | HttpApiResponseException e) { throw e; } catch (Exception e) { log.error("Error de conexion {}: {}", request.getMethod(), e.getMessage()); throw HttpStatusCodeException.serviceUnavailable( "503", "Error de conexion con el servicio externo: " + e.getMessage() ); } } private String buildFinalUrl(HttpRequest request) { String finalUrl = request.getUrl(); if (request.getPathParams() != null && !request.getPathParams().isEmpty()) { for (Map.Entry entry : request.getPathParams().entrySet()) { String placeholder = "{" + entry.getKey() + "}"; finalUrl = finalUrl.replace(placeholder, entry.getValue()); } } return appendQueryParams(finalUrl, request.getQueryParams()); } private String appendQueryParams(String url, Map queryParams) { if (queryParams == null || queryParams.isEmpty()) { return url; } StringBuilder urlBuilder = new StringBuilder(url); boolean firstParam = !url.contains("?"); for (Map.Entry entry : queryParams.entrySet()) { if (firstParam) { urlBuilder.append("?"); firstParam = false; } else { urlBuilder.append("&"); } urlBuilder.append(entry.getKey()) .append("=") .append(entry.getValue() != null ? entry.getValue() : ""); } return urlBuilder.toString(); } private Response buildRequest( Invocation.Builder builder, HttpRequest request ) { log.info("Metodo HTTP: {}", request.getMethod().name()); return switch (request.getMethod()) { case GET -> builder.get(); case POST -> builder.post(Entity.entity(request.getBody(), MediaType.APPLICATION_JSON)); case PUT -> builder.put(Entity.entity(request.getBody(), MediaType.APPLICATION_JSON)); case DELETE -> builder.delete(); case PATCH -> builder.method("PATCH", Entity.entity(request.getBody(), MediaType.APPLICATION_JSON)); case HEAD -> builder.head(); case OPTIONS -> builder.options(); default -> throw new IllegalArgumentException("Metodo HTTP no soportado: " + request.getMethod()); }; } private Client createClient(int connectTimeout, int readTimeout) { return ClientBuilder.newBuilder() .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) .readTimeout(readTimeout, TimeUnit.MILLISECONDS) .build(); } private T handleResponse( HttpRequest request, Response response ) { int statusCode = response.getStatus(); log.info("Respuesta {} - Status: {}", request.getMethod(), statusCode); try (response) { String responseBody = response.readEntity(String.class); if (request.isLogResponseBody()) { log.info("Respuesta Cuerpo: {}", responseBody); } if (statusCode >= 200 && statusCode < 300) { if (request.getResponseType() == Void.class || request.getResponseType() == void.class) { return null; } T result = responseResult(request, responseBody); log.debug("Respuesta exitosa {} {}: {}", request.getMethod(), request.getUrl(), result); return result; } else { log.error( "Error HTTP {} {} - Status: {} - Body: {}", request.getMethod(), request.getUrl(), statusCode, responseBody ); if (isApiResponseFormat(responseBody)) { ApiResponse apiResponse = deserializeApiResponse(responseBody, request); throw new HttpApiResponseException(statusCode, apiResponse); } else { throw mapHttpStatusToException(statusCode, responseBody); } } } catch (HttpStatusCodeException | HttpApiResponseException e) { throw e; } catch (Exception e) { log.error( "Error procesando respuesta {} {}: {}", request.getMethod(), request.getUrl(), e.getMessage() ); throw HttpStatusCodeException.internalServer( "500", "Error procesando respuesta del servicio externo: " + e.getMessage() ); } } private T responseResult( HttpRequest request, String responseBody ) throws JsonProcessingException { if (request.isApiPrivateResponse() && request.isEitherResponse()) { return handleApiPrivateResponseWithEither(request, responseBody); } T result; if (request.getResponseType() == ApiResponse.class) { result = deserializeApiResponse(responseBody, request); } else if (request.getComplexType() != null) { JavaType javaType = objectMapper.getTypeFactory().constructParametricType( request.getResponseType(), objectMapper.getTypeFactory().constructType(request.getComplexType()) ); result = objectMapper.readValue(responseBody, javaType); } else if (request.getGenericType() != null) { JavaType javaType = objectMapper.getTypeFactory().constructParametricType( request.getResponseType(), objectMapper.getTypeFactory().constructType(request.getGenericType()) ); result = objectMapper.readValue(responseBody, javaType); } else { result = objectMapper.readValue( responseBody, objectMapper.getTypeFactory().constructType(request.getResponseType()) ); } return result; } private T handleApiPrivateResponseWithEither( HttpRequest request, String responseBody ) throws JsonProcessingException { JsonNode rootNode = objectMapper.readTree(responseBody); String status = rootNode.has("estatus") ? rootNode.get("estatus").asText() : null; String message = rootNode.has("mensaje") ? rootNode.get("mensaje").asText() : null; JsonNode detailNode = rootNode.get("detalle"); if (request.getStatusSuccess().equals(status)) { return handleSuccessResponse(request, status, message, detailNode); } else { return handleErrorResponse(status, message, detailNode); } } @SuppressWarnings("unchecked") private T handleSuccessResponse( HttpRequest request, String status, String message, JsonNode detailNode ) { Object successData; if (request.isListResponse()) { successData = handleListSuccess(request, detailNode); ApiPrivateResponse, ApiPrivateError>> response = new ApiPrivateResponse<>(); response.setEstatus(status); response.setMensaje(message); response.setDetalle(Either.left((List) successData)); return (T) response; } else { successData = handleObjectSuccess(request, detailNode); ApiPrivateResponse> response = new ApiPrivateResponse<>(); response.setEstatus(status); response.setMensaje(message); response.setDetalle(Either.left(successData)); return (T) response; } } private Object handleListSuccess( HttpRequest request, JsonNode detailNode ) { Class elementType = getElementTypeFromRequest(request); JavaType listType = objectMapper.getTypeFactory().constructCollectionType(List.class, elementType); if (detailNode != null && !detailNode.isNull()) { return objectMapper.convertValue(detailNode, listType); } return List.of(); } private Object handleObjectSuccess( HttpRequest request, JsonNode detailNode ) { Class elementType = getElementTypeFromRequest(request); if (detailNode != null && !detailNode.isNull()) { return objectMapper.convertValue(detailNode, elementType); } return null; } @SuppressWarnings("unchecked") private T handleErrorResponse( String status, String message, JsonNode detailNode ) { ApiPrivateError error = buildApiPrivateError(detailNode, message); ApiPrivateResponse> response = new ApiPrivateResponse<>(); response.setEstatus(status); response.setMensaje(message); response.setDetalle(Either.right(error)); return (T) response; } private ApiPrivateError buildApiPrivateError( JsonNode detailNode, String message ) { if (detailNode != null && !detailNode.isNull()) { try { return objectMapper.convertValue(detailNode, ApiPrivateError.class); } catch (Exception e) { log.warn("Cannot map detail to ApiPrivateError, creating default error. Detail: {}", detailNode); } } return ApiPrivateError.builder() .codError(null) .mensajeError(message) .constraintName(null) .build(); } private Class getElementTypeFromRequest(HttpRequest request) { if (request.getGenericType() != null) { return request.getGenericType(); } if (request.getComplexType() instanceof ParameterizedType pt) { Type[] typeArgs = pt.getActualTypeArguments(); if (typeArgs.length > 0) { Type firstArg = typeArgs[0]; if (firstArg instanceof ParameterizedType innerPt) { Type[] innerArgs = innerPt.getActualTypeArguments(); if (innerArgs.length > 0 && innerArgs[0] instanceof Class innerClass) { return innerClass; } } else if (firstArg instanceof Class elementClass) { return elementClass; } } } log.warn("No se pudo determinar el elementType del request, usando Object.class"); return Object.class; } @SuppressWarnings("unchecked") private T deserializeApiResponse( String responseBody, HttpRequest request ) { try { if (request.getGenericType() != null) { JavaType javaType = objectMapper.getTypeFactory().constructParametricType( ApiResponse.class, objectMapper.getTypeFactory().constructType(request.getGenericType()) ); return objectMapper.readValue(responseBody, javaType); } else { return (T) objectMapper.readValue(responseBody, ApiResponse.class); } } catch (JsonProcessingException e) { log.error("Error deserializando respuesta JSON: {}", e.getMessage()); throw HttpStatusCodeException.internalServer( "500", "Error deserializando respuesta JSON: " + e.getMessage() ); } catch (Exception e) { log.error("Error desconocido al deserializar respuesta: {}", e.getMessage()); throw HttpStatusCodeException.internalServer( "500", "Error desconocido al deserializar respuesta: " + e.getMessage() ); } } private boolean isApiResponseFormat(String responseBody) { try { if (responseBody == null || responseBody.trim().isEmpty()) { return false; } return responseBody.contains("\"data\"") && responseBody.contains("\"statusResponse\"") && responseBody.contains("\"statusCode\"") && responseBody.contains("\"message\""); } catch (Exception e) { return false; } } private HttpStatusCodeException mapHttpStatusToException( int statusCode, String errorBody ) { String errorCode = "HTTP_" + statusCode; String defaultMessage = "Error en servicio externo: HTTP " + statusCode; String message = errorBody != null && !errorBody.isEmpty() ? errorBody : defaultMessage; return switch (statusCode) { case 400 -> HttpStatusCodeException.badRequest(errorCode, message); case 401 -> HttpStatusCodeException.unauthorized(errorCode, message); case 403 -> HttpStatusCodeException.forbidden(errorCode, message); case 404 -> HttpStatusCodeException.notFound(errorCode, message); case 405 -> HttpStatusCodeException.methodNotAllowed(errorCode, message); case 408 -> HttpStatusCodeException.fromStatusCode(408, errorCode, message); case 409 -> HttpStatusCodeException.conflict(errorCode, message); case 410 -> HttpStatusCodeException.gone(errorCode, message); case 412 -> HttpStatusCodeException.preconditionFailed(errorCode, message); case 415 -> HttpStatusCodeException.unsupportedMediaType(errorCode, message); case 422 -> HttpStatusCodeException.unprocessableEntity(errorCode, message); case 429 -> HttpStatusCodeException.tooManyRequests(errorCode, message); case 500 -> HttpStatusCodeException.internalServer(errorCode, message); case 501 -> HttpStatusCodeException.notImplemented(errorCode, message); case 502 -> HttpStatusCodeException.badGateway(errorCode, message); case 503 -> HttpStatusCodeException.serviceUnavailable(errorCode, message); case 504 -> HttpStatusCodeException.gatewayTimeout(errorCode, message); default -> HttpStatusCodeException.fromStatusCode(statusCode, errorCode, message); }; } }