package com.cf.imes.gateway.filter.security;

import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.cf.imes.framework.common.exception.ErrorCode;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.common.util.json.JsonUtils;
import com.cf.imes.gateway.config.CfGatewayProperties;
import com.cf.imes.gateway.util.SecurityFrameworkUtils;
import com.cf.imes.gateway.util.WebFrameworkUtils;
import com.cf.imes.module.system.api.oauth2.OAuth2TokenApi;
import com.cf.imes.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
import com.fasterxml.jackson.core.type.TypeReference;
import jakarta.annotation.Resource;
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.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Locale;
import java.util.Objects;
import java.util.function.Function;

import static com.cf.imes.module.system.enums.ErrorCodeConstants.*;

/**
 * Token 过滤器，验证 token 的有效性
 * 1. 验证通过时，将 userId、userType、organId 通过 Header 转发给服务
 * 2. 验证不通过，还是会转发给服务。因为，接口是否需要登录的校验，还是交给服务自身处理
 *
 * @author 晨丰科技
 */
@Component
public class TokenAuthenticationFilter implements GlobalFilter, Ordered {

    /**
     * CommonResult<OAuth2AccessTokenCheckRespDTO> 对应的 TypeReference 结果，用于解析 checkToken 的结果
     */
    private static final TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>> CHECK_RESULT_TYPE_REFERENCE
            = new TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>>() {};


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

    private static final TypeReference<CommonResult<String>> IGNORE_TOKEN_SOURCE_RESULT
            = new TypeReference<>() {
    };

    @Resource
    private CfGatewayProperties cfGatewayProperties;

    /**
     * 空的 LoginUser 的结果
     *
     * 用于解决如下问题：
     * 1. {@link #getLoginUser(ServerWebExchange, String)} 返回 Mono.empty() 时，会导致后续的 flatMap 无法进行处理的问题。
     * 2. {@link #buildUser(String)} 时，如果 Token 已经过期，返回 LOGIN_USER_EMPTY 对象，避免缓存无法刷新
     */
    private static final LoginUser LOGIN_USER_EMPTY = new LoginUser();

    private final WebClient webClient;

    public TokenAuthenticationFilter(ReactorLoadBalancerExchangeFilterFunction lbFunction) {
        // Q：为什么不使用 OAuth2TokenApi 进行调用？
        // A1：Spring Cloud OpenFeign 官方未内置 Reactive 的支持 https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#reactive-support
        // A2：校验 Token 的 API 需要使用到 header[organ-id] 传递组织编号，暂时不想编写 RequestInterceptor 实现
        // 因此，这里采用 WebClient，通过 lbFunction 实现负载均衡
        this.webClient = WebClient.builder().filter(lbFunction).build();
    }

    @Override
    public Mono<Void> filter(final ServerWebExchange exchange, GatewayFilterChain chain) {
        // 移除 login-user 的请求头，避免伪造模拟
        SecurityFrameworkUtils.removeLoginUser(exchange);

        // 情况一，如果没有 Token 令牌，则直接继续 filter
        String token = SecurityFrameworkUtils.obtainAuthorization(exchange);
        if (CharSequenceUtil.isEmpty(token)) {
            // 是否忽略token的外部请求，如小程序用
            AntPathMatcher pathMatcher = new AntPathMatcher();
            String url = exchange.getRequest().getURI().getPath();
            for (String ignoreUrl : cfGatewayProperties.getIgnoreTokenUrls()) {
                if (pathMatcher.match(ignoreUrl, url)) {
                    return getIgnoreTokenSource()
                            .defaultIfEmpty("")
                            .flatMap(result -> {
                                String dataCode = buildSource(result);
                                LoginUser user = new LoginUser().setDataCode(dataCode);
                                ServerWebExchange newExchange = exchange.mutate()
                                        .request(builder -> SecurityFrameworkUtils.setLoginUserHeader1(builder, user))
                                        .build();
                                return chain.filter(newExchange);
                            });
                }
            }

            return chain.filter(exchange);
        }

        // 情况二，如果有 Token 令牌，则解析对应 userId、userType、organId 等字段，并通过 通过 Header 转发给服务
        // 重要说明：defaultIfEmpty 作用，保证 Mono.empty() 情况，可以继续执行 `flatMap 的 chain.filter(exchange)` 逻辑，避免返回给前端空的 Response！！
        return getLoginUser(exchange, token).defaultIfEmpty(LOGIN_USER_EMPTY).flatMap(user -> {
            // 1. 无用户，直接 filter 继续请求
            if (user == LOGIN_USER_EMPTY) {
                return chain.filter(exchange);
            }

            // 2.1 有用户，则设置登录用户
            SecurityFrameworkUtils.setLoginUser(exchange, user);
            // 2.2 将 user 并设置到 login-user 的请求头，使用 json 存储值
            ServerWebExchange newExchange = exchange.mutate()
                    .request(builder -> SecurityFrameworkUtils.setLoginUserHeader(builder, user))
                    .build();

            return chain.filter(newExchange);
        });
    }

    private Mono<LoginUser> getLoginUser(ServerWebExchange exchange, String token) {
        // 用token从checktoken api中获取当前用户
        Long organId = WebFrameworkUtils.getOrganId(exchange);
        return checkAccessToken(organId, token).flatMap((Function<String, Mono<LoginUser>>) body -> {
            String language = exchange.getRequest().getHeaders().getFirst("Accept-Language");
            Locale locale;
            if(StrUtil.isEmpty(language)) {
                locale = Locale.CHINA;
            } else {
                locale = Locale.forLanguageTag(language);
            }
            LoginUser remoteUser = buildUser(body, locale);
            if (remoteUser != null) {
                return Mono.just(remoteUser);
            }
            return Mono.empty();
        });
    }

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

    private Mono<String> getIgnoreTokenSource() {
        return webClient.get()
                .uri(OAuth2TokenApi.IGNORE_TOKEN_SOURCE_GET)
                .retrieve().bodyToMono(String.class);
    }

    private LoginUser buildUser(String body, Locale locale) {

        CommonResult commonResult = JsonUtils.parseObject(body, TOKEN_RESULT);
        if(commonResult != null) {
            Integer code = commonResult.getCode();
            String msg = commonResult.getMsg();
            Object[] args = commonResult.getArgs();
            if(code != 0) {
                if (TOKEN_TIME_IS_EXPIRES.getCode().equals(code)) {
                    throw new ServiceException(TOKEN_TIME_IS_EXPIRES, locale);
                } else if (TOKEN_CONFIG_NOT_EXISTS.getCode().equals(code)) {
                    throw new ServiceException(TOKEN_CONFIG_NOT_EXISTS, locale);
                } else if (ORGAN_NOT_EXISTS.getCode().equals(code)) {
                    throw new ServiceException(ORGAN_NOT_EXISTS, locale);
                } else if (ORGAN_DISABLE.getCode().equals(code)) {
                    throw new ServiceException(ORGAN_DISABLE, locale);
                } else if (ORGAN_EXPIRE.getCode().equals(code)) {
                    throw new ServiceException(ORGAN_EXPIRE, locale);
                }
            }
        }
        // 处理结果，结果不正确
        CommonResult<OAuth2AccessTokenCheckRespDTO> result = JsonUtils.parseObject(body, CHECK_RESULT_TYPE_REFERENCE);
        if (result == null) {
            return null;
        }
        if (result.isError()) {
            // 特殊情况：令牌已经过期（code = 401），需要返回 LOGIN_USER_EMPTY，避免 Token 一直因为缓存，被误判为有效
            if (Objects.equals(result.getCode(), HttpStatus.UNAUTHORIZED.value())) {
                return LOGIN_USER_EMPTY;
            }
            return null;
        }

        // 创建登录用户
        OAuth2AccessTokenCheckRespDTO tokenInfo = result.getData();
        return new LoginUser().setId(tokenInfo.getUserId()).setUserType(tokenInfo.getUserType())
                .setOrganId(tokenInfo.getOrganId()).setScopes(tokenInfo.getScopes())
                .setLarge(tokenInfo.getLarge()).setDbNo(tokenInfo.getDbNo()).setTableNo(tokenInfo.getTableNo())
                .setDataCode(tokenInfo.getDataCode()).setIsSupAdmin(tokenInfo.getIsSupAdmin())
                .setNickname(tokenInfo.getNickname()).setDeptId(tokenInfo.getDeptId())
                .setGrayVersion(tokenInfo.getGrayVersion()).setGrayDefaultVersion(tokenInfo.getGrayDefaultVersion())
                .setProductId(tokenInfo.getProductId());
    }

    /**
     * 构建忽略令牌的数据源请求结果
     *
     * @param body
     * @return
     */
    private String buildSource(String body) {
        CommonResult<String> commonResult = JsonUtils.parseObject(body, IGNORE_TOKEN_SOURCE_RESULT);
        if (ObjectUtil.isNotNull(commonResult)) {
            return commonResult.getData();
        } else {
            return null;
        }
    }

    @Override
    public int getOrder() {
        return -100; // 和 Spring Security Filter 的顺序对齐
    }

}
