package com.njcn.rdms.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.rdms.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
import com.njcn.rdms.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
import com.njcn.rdms.framework.common.exception.ServiceException;
import com.njcn.rdms.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.date.LocalDateTimeUtils;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.gateway.util.SecurityFrameworkUtils;
import com.njcn.rdms.gateway.util.WebFrameworkUtils;
import com.njcn.rdms.module.system.enums.ErrorCodeConstants;
import lombok.extern.slf4j.Slf4j;
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.Set;
import java.util.function.Function;

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

@Slf4j
@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();

    /**
     * 跳过 access token 校验的路径白名单。
     * 这些接口在 system 端标注 @PermitAll，本就不需要登录态；若前端调用时带过期 access，
     * 网关不应在此处拦截 1002023000，否则 /refresh-token 永远走不到 system 的 1002023001 / 业务逻辑。
     */
    private static final Set<String> SKIP_AUTH_PATHS = Set.of(
            "/admin-api/system/auth/login",
            "/admin-api/system/auth/logout",
            "/admin-api/system/auth/refresh-token",
            "/admin-api/system/auth/register"
    );

    private final WebClient webClient;

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

                @Override
                public LoginUser load(String token) {
                    // 仅异步 refresh 走这里（同步链路用 getIfPresent + 直接 checkAccessToken，不触发 load）
                    // 远端 token 已过期/校验失败时吞掉 ServiceException：
                    // 若抛出，会被 Guava 包成 ExecutionException 并由刷新线程池作为 UncaughtException 打到日志，看起来像故障。
                    try {
                        String body = checkAccessToken(token).block();
                        return buildUser(body, token);
                    } catch (ServiceException ex) {
                        log.info("[loginUserCache] 异步刷新忽略 token 校验失败：code={}, msg={}", ex.getCode(), ex.getMessage());
                        return LOGIN_USER_EMPTY;
                    }
                }

            });

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

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

        // 白名单路径直接放行，不做 token 校验
        if (SKIP_AUTH_PATHS.contains(exchange.getRequest().getPath().value())) {
            return chain.filter(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;
    }

}
