package com.njcn.msgpush.gateway.filter.security;

import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.njcn.msgpush.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
import com.njcn.msgpush.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
import com.njcn.msgpush.framework.common.exception.ServiceException;
import com.njcn.msgpush.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.njcn.msgpush.framework.common.pojo.CommonResult;
import com.njcn.msgpush.framework.common.util.date.LocalDateTimeUtils;
import com.njcn.msgpush.framework.common.util.json.JsonUtils;
import com.njcn.msgpush.gateway.util.SecurityFrameworkUtils;
import com.njcn.msgpush.gateway.util.WebFrameworkUtils;
import com.njcn.msgpush.module.system.enums.ErrorCodeConstants;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.Objects;
import java.util.function.Function;

import static com.njcn.msgpush.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;

@Component
public class TokenAuthenticationFilter implements GlobalFilter, Ordered {

    private static final TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>> CHECK_RESULT_TYPE_REFERENCE =
            new TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>>() {};

    private static final LoginUser LOGIN_USER_EMPTY = new LoginUser();

    private final WebClient webClient;

    private final LoadingCache<String, LoginUser> loginUserCache = buildAsyncReloadingCache(Duration.ofMinutes(1),
            new CacheLoader<String, LoginUser>() {

                @Override
                public LoginUser load(String token) {
                    String body = checkAccessToken(token).block();
                    return buildUser(body, token);
                }

            });

    public TokenAuthenticationFilter(ReactorLoadBalancerExchangeFilterFunction lbFunction) {
        this.webClient = WebClient.builder().filter(lbFunction).build();
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        exchange = SecurityFrameworkUtils.removeLoginUser(exchange);

        String token = SecurityFrameworkUtils.obtainAuthorization(exchange);
        if (StrUtil.isEmpty(token)) {
            return chain.filter(exchange);
        }

        ServerWebExchange finalExchange = exchange;
        return getLoginUser(token)
                .flatMap(user -> {
                    if (user.getExpiresTime() == null) {
                        return chain.filter(finalExchange);
                    }
                    if (LocalDateTimeUtils.beforeNow(user.getExpiresTime())) {
                        loginUserCache.invalidate(token);
                        return WebFrameworkUtils.writeJSON(finalExchange,
                                CommonResult.error(ErrorCodeConstants.OAUTH2_ACCESS_TOKEN_EXPIRE));
                    }

                    SecurityFrameworkUtils.setLoginUser(finalExchange, user);
                    ServerWebExchange newExchange = finalExchange.mutate()
                            .request(builder -> SecurityFrameworkUtils.setLoginUserHeader(builder, user))
                            .build();
                    return chain.filter(newExchange);
                })
                .switchIfEmpty(Mono.defer(() -> chain.filter(finalExchange)))
                .onErrorResume(ServiceException.class, ex ->
                        WebFrameworkUtils.writeJSON(finalExchange, CommonResult.error(ex)));
    }

    private Mono<LoginUser> getLoginUser(String token) {
        LoginUser localUser = loginUserCache.getIfPresent(token);
        if (localUser != null) {
            return localUser == LOGIN_USER_EMPTY ? Mono.empty() : Mono.just(localUser);
        }

        return checkAccessToken(token).flatMap((Function<String, Mono<LoginUser>>) body -> {
            LoginUser remoteUser = buildUser(body, token);
            if (remoteUser != null) {
                loginUserCache.put(token, remoteUser);
                return Mono.just(remoteUser);
            }
            return Mono.empty();
        });
    }

    private Mono<String> checkAccessToken(String token) {
        return webClient.get()
                .uri(OAuth2TokenCommonApi.URL_CHECK, uriBuilder -> uriBuilder.queryParam("accessToken", token).build())
                .retrieve()
                .bodyToMono(String.class);
    }

    private LoginUser buildUser(String body, String token) {
        CommonResult<OAuth2AccessTokenCheckRespDTO> result = JsonUtils.parseObject(body, CHECK_RESULT_TYPE_REFERENCE);
        if (result == null) {
            return null;
        }
        if (result.isError()) {
            if (Objects.equals(result.getCode(), GlobalErrorCodeConstants.UNAUTHORIZED.getCode())) {
                loginUserCache.put(token, LOGIN_USER_EMPTY);
                return LOGIN_USER_EMPTY;
            }
            throw new ServiceException(result.getCode(), result.getMsg());
        }

        OAuth2AccessTokenCheckRespDTO tokenInfo = result.getData();
        return new LoginUser().setId(tokenInfo.getUserId())
                .setUserType(tokenInfo.getUserType())
                .setInfo(tokenInfo.getUserInfo())
                .setScopes(tokenInfo.getScopes())
                .setExpiresTime(tokenInfo.getExpiresTime());
    }

    @Override
    public int getOrder() {
        return -100;
    }

}
