package com.red.circle.gateway.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.dto.ReqAppIntel;
import com.red.circle.framework.core.dto.ReqInternal;
import com.red.circle.framework.core.dto.ReqSysOrigin;
import com.red.circle.framework.core.request.RequestClientEnum;
import com.red.circle.framework.core.request.RequestHeaderConstant;
import com.red.circle.framework.core.response.FrameworkWebErrorCode;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.gateway.config.GatewayProperties;
import com.red.circle.gateway.constant.ApiConstant;
import com.red.circle.gateway.service.AuthEndpointService;
import com.red.circle.gateway.service.UserCredential;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.DateUtils;
import com.red.circle.tool.core.text.StringPool;
import com.red.circle.tool.core.text.StringUtils;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 全局拦截器，应用所有服务.
 *
 * @author pengliang on 2023/4/18
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {

  private final ObjectMapper objectMapper;
  private final GatewayProperties gatewayProperties;
  private final AuthEndpointService authEndpointService;
  private final PathMatcher pathMatcher = new AntPathMatcher();
  private final ResultResponse<UserCredential> reqFailUserCredential = ResultResponse.failure(503,
      "Request timed out",
      "REQ_FAIL");

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    long nowTime = DateUtils.now().getTime();

    if (isIgnoreMatcherPath(exchange)) {
      return returnDefaultExchange(exchange, chain, nowTime);
    }

    HttpHeaders reqHttpHeaders = exchange.getRequest().getHeaders();
    ResponseAssert.notEmpty(FrameworkWebErrorCode.REQ_SYS_ORIGIN, reqHttpHeaders);

    String authorization = getAuthorization(exchange);
    if (checkAuthorizationNotPass(authorization)) {
      return responseUnauthorized401(exchange);
    }

    checkRequestRequiredHeader(exchange);

    exchange.getAttributes().put(ApiConstant.REQ_URL, ApiConstant.AUTH_SERVICE_API);
    return authEndpointService.getTokenCredential(authorization)
        .defaultIfEmpty(reqFailUserCredential)
        .flatMap(response -> response.optionalBody().map(userCredential -> {
                  ServerHttpRequest request = exchange.getRequest().mutate()
                      .headers(httpHeaders -> addHeaders(httpHeaders,
                          createReqInternal(nowTime, userCredential))).build();
                  exchange.getAttributes().put(ApiConstant.REQ_URL, request.getURI().getRawPath());
                  return chain.filter(exchange.mutate().request(request).build());
                }
            ).orElseGet(() -> responseUnauthorized401(exchange))
        );
  }

  private boolean checkAuthorizationNotPass(String authorization) {
    try {
      boolean isPass = StringUtils.isNotBlank(authorization)
          && com.red.circle.framework.core.security.UserCredential.parseToken(
          authorization).checkSign();
      return !isPass;
    } catch (Exception ignore) {
      // ignore
    }
    return true;
  }

  /**
   * 验证必填请求头参数.
   * <p>同步: com.red.circle.framework.web.spring.ApplicationRequestUtils</p>
   */
  private void checkRequestRequiredHeader(ServerWebExchange exchange) {

    // Req-Sys-Origin
    ReqSysOrigin reqSysOrigin = ReqSysOrigin.parse(getHeaderFirstNotBankVal(exchange,
        RequestHeaderConstant.REQ_SYS_ORIGIN));
    ResponseAssert.notNull(FrameworkWebErrorCode.REQ_SYS_ORIGIN, reqSysOrigin);
    ResponseAssert.notBlank(FrameworkWebErrorCode.REQ_SYS_ORIGIN, reqSysOrigin.getOrigin());

    // Req-Client
    RequestClientEnum reqClient = RequestClientEnum.getAppPlatformEnum(
        getHeaderFirstNotBankVal(exchange,
            RequestHeaderConstant.REQ_CLIENT));
    ResponseAssert.notNull(FrameworkWebErrorCode.REQ_CLIENT, reqClient);

    // Req-App-Intel
    ReqAppIntel reqAppIntel = ReqAppIntel.parse(getHeaderFirstNotBankVal(exchange,
        RequestHeaderConstant.REQ_APP_INTEL));
    ResponseAssert.notNull(FrameworkWebErrorCode.REQ_APP_INTEL, reqAppIntel);
    ResponseAssert.notBlank(FrameworkWebErrorCode.REQ_APP_INTEL_VERSION,
        reqAppIntel.getVersion());
    ResponseAssert.notNull(FrameworkWebErrorCode.REQ_APP_INTEL_BUILD, reqAppIntel.getBuild());
    ResponseAssert.isTrue(FrameworkWebErrorCode.REQ_APP_INTEL_BUILD,
        reqAppIntel.getBuild() > 0);
    ResponseAssert.notBlank(FrameworkWebErrorCode.REQ_APP_INTEL_CHANNEL,
        reqAppIntel.getChannel());
  }

  private Mono<Void> returnDefaultExchange(ServerWebExchange exchange, GatewayFilterChain chain,
      long nowTime) {
    ServerHttpRequest request = exchange.getRequest().mutate()
        .headers(httpHeaders -> addHeaders(httpHeaders, createReqInternal(nowTime, null)))
        .build();
    return chain.filter(exchange.mutate().request(request).build());
  }

  private String getAuthorization(ServerWebExchange exchange) {
    String authorization = getHeaderFirstNotBankVal(exchange, RequestHeaderConstant.AUTHORIZATION);

    return StringUtils.isBlank(authorization)
        ? authorization
        : authorization.replaceFirst("Bearer", "").trim();
  }

  private String getHeaderFirstNotBankVal(ServerWebExchange exchange, String headKey) {
    return Optional.ofNullable(
            exchange.getRequest().getHeaders().get(headKey))
        .filter(CollectionUtils::isNotEmpty)
        .map(auths -> auths.stream().filter(StringUtils::isNotBlank)
            .findFirst().orElse(StringPool.EMPTY))
        .orElse(StringPool.EMPTY);
  }

  private ReqInternal createReqInternal(long nowTime, UserCredential userCredential) {
    ReqInternal reqInternal = new ReqInternal();
    reqInternal.setTime(nowTime);
    if (Objects.nonNull(userCredential)) {
      reqInternal.setUid(Objects.toString(userCredential.getUserId()));
    }
    return reqInternal;
  }

  private void addHeaders(HttpHeaders httpHeaders, ReqInternal reqInternal) {
    httpHeaders.add(RequestHeaderConstant.REQ_INNER_INTERNAL, reqInternal.toTransferVal());
  }


  @Override
  public int getOrder() {
    return 10;
  }

  private Mono<Void> responseUnauthorized401(ServerWebExchange exchange) {
    String unauthorizedErrorMessage = "Please first authenticate";
    return response(exchange, HttpStatusCode.valueOf(401), unauthorizedErrorMessage);
  }

  private Mono<Void> response(ServerWebExchange exchange, HttpStatusCode code, String message) {
    ServerHttpResponse response = exchange.getResponse();

    if (response.isCommitted()) {
      return Mono.error(new RuntimeException(message));
    }

    // header set
    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
    response.setStatusCode(code);

    return response.writeWith(Mono.fromSupplier(() -> {
      DataBufferFactory bufferFactory = response.bufferFactory();
      try {
        return bufferFactory.wrap(objectMapper.writeValueAsBytes(
            ResultResponse.failure(Optional.ofNullable(response.getStatusCode())
                    .map(HttpStatusCode::value)
                    .orElse(-1),
                message, "UNAUTHORIZED")));
      } catch (JsonProcessingException ex) {
        log.error("Error writing response", ex);
        return bufferFactory.wrap(new byte[0]);
      }
    }));
  }

  private boolean isIgnoreMatcherPath(ServerWebExchange exchange) {
    String url = exchange.getRequest().getURI().getRawPath();
    log.debug(url);
    List<String> ignorePaths = gatewayProperties.getIgnorePaths();
    log.debug(ignorePaths.toString());
    for (String ignoreUrl : ignorePaths) {
      if (pathMatcher.match(ignoreUrl, url)) {
        log.debug("true : {}{}",url, ignoreUrl);
        return true;
      }
    }
    log.debug("false : {}",url);
    return false;
  }

  public static void main(String[] args) {
    PathMatcher pathMatcher = new AntPathMatcher();

    List<String> ignorePaths = Arrays.asList("/token/check/account/login", "/play-server-notice/google/YOLO");
    String url = "/play-server-notice/google/YOLO";
    for (String ignoreUrl : ignorePaths) {
      System.out.println(pathMatcher.match(ignoreUrl, url));
      if (pathMatcher.match(ignoreUrl, url)) {
        System.out.println("true");
      }
    }
  }
}
