package com.red.circle.framework.feign.decoder;

import com.fasterxml.jackson.core.type.TypeReference;
import com.red.circle.framework.core.exception.ResponseException;
import com.red.circle.framework.core.request.ResponseHeaderConstant;
import com.red.circle.framework.core.response.ResponseErrorCode;
import com.red.circle.framework.core.spring.ApplicationContextUtils;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.tool.core.http.HttpConstant;
import com.red.circle.tool.core.json.JacksonUtils;
import com.red.circle.tool.core.text.StringUtils;
import feign.Request;
import feign.Response;
import feign.codec.ErrorDecoder;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class FeignErrorDecoder implements ErrorDecoder {

  private static final Logger log = LoggerFactory.getLogger(FeignErrorDecoder.class);

  private static final List<Integer> PROCESS_RESULT_RESPONSE = List.of(
      ResponseErrorCode.NOT_ACCEPTABLE.getCode(),
      ResponseErrorCode.REQUEST_PARAMETER_ERROR.getCode()
  );

  @Override
  public Exception decode(String methodKey, Response response) {
    if (PROCESS_RESULT_RESPONSE.stream()
        .anyMatch(code -> Objects.equals(response.status(), code))) {
      ResponseException exception = parseResponseException(methodKey, response);
      if (Objects.nonNull(exception)) {
        return exception;
      }
      return createResponseException(methodKey, response);
    }

    if (HttpConstant.is4xx(response.status())) {
      return createResponseException(methodKey, response);
    }

    if (ResponseHeaderConstant.existsHeaderValue(response.headers(), "RC-No-Retry", "true")) {
      ResponseException exception = parseResponseException(methodKey, response);
      if (Objects.nonNull(exception)) {
        return exception;
      }
      return createResponseException(methodKey, response);
    }

    return createResponseException(methodKey, response);
  }

  private static String reqMapping(Response response) {
    Request request = response.request();
    if (Objects.isNull(request)) {
      return "";
    }
    return request.httpMethod() + " " + request.url();
  }

  private ResponseException createResponseException(String methodKey, Response response) {
    String errorCodeName = Objects.equals(response.status(), 429)
        ? "REQ_LIMIT_INNER_DECODER"
        : "ERROR_INNER_DECODER";
    Map<String, Object> extValues = new HashMap<>();
    extValues.put("innerRequest", reqMapping(response));
    extValues.put("innerMethodKey", methodKey);
    return new ResponseException(response.status(), errorCodeName, getErrorMsg(response), extValues);
  }

  private String getErrorMsg(Response response) {
    return StringUtils.isBlankOrElse(response.reason(), "Inner service error");
  }

  private ResponseException parseResponseException(String methodKey, Response response) {
    ResultResponse<Void> responseBody = parseResponse(response);
    if (Objects.nonNull(responseBody)) {
      Map<String, Object> extValues = Optional.ofNullable(responseBody.getExtValues())
          .orElseGet(HashMap::new);

      extValues.put("innerError",
          "[" + methodKey + "]" + StringUtils.isBlankOrElse(response.reason(), ""));

      return new ResponseException(
          responseBody.getErrorCode(),
          responseBody.getErrorCodeName(),
          responseBody.getErrorMsg(),
          extValues
      );
    }
    return null;
  }

  private ResultResponse<Void> parseResponse(Response response) {
    if (Objects.isNull(response.body())) {
      log.warn("Feign error response body is null, status={}, reason={}, request={}",
          response.status(), response.reason(), reqMapping(response));
      return null;
    }

    try (InputStream inputStream = response.body().asInputStream()) {
      byte[] bodyBytes = inputStream.readAllBytes();
      if (bodyBytes.length == 0) {
        log.warn("Feign error response body is empty, status={}, reason={}, request={}",
            response.status(), response.reason(), reqMapping(response));
        return null;
      }
      boolean gzipEncoded = isGzipEncoded(response);
      ResultResponse<Void> responseBody = parseResponseBytes(response, bodyBytes, gzipEncoded);
      if (Objects.nonNull(responseBody)) {
        return responseBody;
      }
      return parseResponseBytes(response, bodyBytes, !gzipEncoded);
    } catch (IOException | RuntimeException ex) {
      log.warn("Read feign error response body failed, status={}, reason={}, request={}",
          response.status(), response.reason(), reqMapping(response), ex);
      return null;
    }
  }

  private ResultResponse<Void> parseResponseBytes(
      Response response,
      byte[] bodyBytes,
      boolean compression) {
    try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bodyBytes)) {
      return JacksonUtils.readValue(inputStream, new TypeReference<>() {
      }, compression);
    } catch (IOException | RuntimeException ex) {
      log.warn("Parse feign error response body failed, status={}, reason={}, request={}, compression={}",
          response.status(), response.reason(), reqMapping(response), compression, ex);
      return null;
    }
  }

  private boolean isGzipEncoded(Response response) {
    return response.headers().entrySet().stream()
        .filter(entry -> Objects.nonNull(entry.getKey()))
        .filter(entry -> "Content-Encoding".equalsIgnoreCase(entry.getKey()))
        .flatMap(entry -> entry.getValue().stream())
        .anyMatch(value -> "gzip".equalsIgnoreCase(value));
  }
}
