package com.red.circle.gateway.handler;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.red.circle.framework.core.exception.ResponseException;
import com.red.circle.framework.core.request.ServiceTraceId;
import com.red.circle.framework.core.request.ServiceTraceId.Origin;
import com.red.circle.framework.core.response.ResponseErrorCode;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.gateway.constant.ApiConstant;
import com.red.circle.gateway.logging.GatewayLogging;
import com.red.circle.gateway.logging.GatewayLoggingResponse;
import com.red.circle.gateway.logging.LoggingUploadService;
import com.red.circle.gateway.util.ApiLoggingUtils;
import com.red.circle.tool.core.json.JacksonUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.util.Objects;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.handler.ResponseStatusExceptionHandler;
import reactor.core.publisher.Mono;

/**
 * 网关异常通用处理器,只作用在webflux 环境下 , 优先级低于 {@link ResponseStatusExceptionHandler} 执行.
 *
 * @author pengliang on 2023/4/18
 */
@Slf4j
@Order(-1)
@RequiredArgsConstructor
@Component
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {

  private final ObjectMapper objectMapper;
  private static final int MAX_HTTP_CODE = 600;
  private final LoggingUploadService loggingUploadService;

  @Override
  public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
    ServerHttpResponse response = exchange.getResponse();

    if (response.isCommitted()) {
      return Mono.error(ex);
    }

    GatewayLogging logging = ApiLoggingUtils.createGatewayLogging(exchange);
    if (StringUtils.isBlank(logging.getId())) {
      logging.setId(ServiceTraceId.genTraceId(Origin.ERROR));
    }
    // 请求参数, 非body类型
    logging.getRequest().setBody(ApiLoggingUtils.getRequestParamsToString(exchange));

    ResultResponse<Object> errorResult = ResultResponse.failure(
            getResponseErrorCode(response),
            StringUtils.isBlankOrElse(ex.getMessage(),
                "Internal abnormality of gateway link"),
            "EXCEPTION")
        .putExtField("service", "gateway")
        .putExtField("url", exchange.getAttributes().get(ApiConstant.REQ_URL));

    // header set
    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
    if (ex instanceof ResponseStatusException rsStatusEx) {
      response.setStatusCode(rsStatusEx.getStatusCode());
      if (Objects.equals(errorResult.getErrorCode(), 200)) {
        errorResult.setErrorCode(rsStatusEx.getStatusCode().value());
      }
    }

    if (ex instanceof ResponseException responseEx) {
      response.setStatusCode(
          Objects.nonNull(responseEx.getErrorCode()) && responseEx.getErrorCode() < MAX_HTTP_CODE
              ? HttpStatusCode.valueOf(responseEx.getErrorCode())
              : HttpStatusCode.valueOf(ResponseErrorCode.NOT_ACCEPTABLE.getCode()));

      errorResult.copy(
          ResultResponse.failure(
              responseEx.getErrorCode(),
              responseEx.getErrorMsg(),
              responseEx.getErrorCodeName()
          )
      );
    }

    if (ex instanceof NotFoundException responseEx) {
      errorResult.setErrorMsg(StringUtils.isBlankOrElse(
              responseEx.getReason(),
              responseEx.getMessage()
          )
      );
    }

    if (ex instanceof WebClientResponseException webClientRes) {
      response.setStatusCode(webClientRes.getStatusCode());
      ResultResponse<Object> responseError = JacksonUtils.readValue(
          webClientRes.getResponseBodyAsString(),
          new TypeReference<>() {
          });

      if (Objects.nonNull(responseError)) {
        errorResult.copy(responseError);
      }
    }

    HttpStatusCode statusCode = response.getStatusCode();
    int httpCodeOk = 200;
    if (Objects.nonNull(statusCode) && Objects.equals(statusCode.value(), httpCodeOk)) {
      response.setStatusCode(
          HttpStatusCode.valueOf(ResponseErrorCode.INTERNAL_SERVER_ERROR.getCode()));
      errorResult.setErrorCode(ResponseErrorCode.INTERNAL_SERVER_ERROR.getCode());
    }

    if (Objects.equals(errorResult.getErrorCode(), httpCodeOk)) {
      errorResult.setErrorCode(Optional.ofNullable(statusCode)
          .map(HttpStatusCode::value).orElse(ResponseErrorCode.INTERNAL_SERVER_ERROR.getCode()));
    }

    return response.writeWith(Mono.fromSupplier(() -> {
      DataBufferFactory bufferFactory = response.bufferFactory();
      GatewayLoggingResponse loggingResponse = logging.getResponse();
      loggingResponse.setCode(Optional.ofNullable(response.getStatusCode())
          .map(HttpStatusCode::value).map(Objects::toString).orElse("?")
      );
      loggingResponse.setBody(JacksonUtils.toJson(errorResult));
      loggingResponse.setTime(System.currentTimeMillis());
      logging.calculateExecuteTime();
      logging.uploadLog(loggingUploadService);
      try {
        return bufferFactory.wrap(objectMapper.writeValueAsBytes(errorResult));
      } catch (JsonProcessingException e) {
        log.error("Error writing response", ex);
        return bufferFactory.wrap(new byte[0]);
      }
    }));
  }

  private static int getResponseErrorCode(ServerHttpResponse response) {
    HttpStatusCode httpStatusCode = response.getStatusCode();
    return Objects.nonNull(httpStatusCode) ? httpStatusCode.value()
        : ResponseErrorCode.INTERNAL_SERVER_ERROR.getCode();
  }

}
