package com.ankaibei.workFlow.webadmin.interceptor;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.ankaibei.workFlow.webadmin.config.ApplicationConfig;
import com.ankaibei.workFlow.common.core.cache.CacheConfig;
import com.ankaibei.workFlow.common.core.annotation.NoAuthInterface;
import com.ankaibei.workFlow.common.core.constant.ApplicationConstant;
import com.ankaibei.workFlow.common.core.constant.ErrorCodeEnum;
import com.ankaibei.workFlow.common.core.object.ResponseResult;
import com.ankaibei.workFlow.common.core.object.TokenData;
import com.ankaibei.workFlow.common.core.util.ApplicationContextHolder;
import com.ankaibei.workFlow.common.core.util.JwtUtil;
import com.ankaibei.workFlow.common.core.util.RedisKeyUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RSet;
import org.redisson.api.RedissonClient;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Set;

/**
 * 登录用户Token验证、生成和权限验证的拦截器。
 *
 * @author Buzhanyi
 * @date 2022-09-25
 */
@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {

    private final ApplicationConfig appConfig =
            ApplicationContextHolder.getBean("applicationConfig");

    private final RedissonClient redissonClient = ApplicationContextHolder.getBean(RedissonClient.class);

    private final CacheManager cacheManager = ApplicationContextHolder.getBean("caffeineCacheManager");

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        String url = request.getRequestURI();
        log.info("拦截**请求路径：{}",url);
        if(url.contains("/foreign/")){
            return true;
        }
        String token = this.getTokenFromRequest(request);
        boolean noLoginUrl = this.isNoAuthInterface(handler);
        // 如果接口方法标记NoAuthInterface注解，可以直接跳过Token鉴权验证，这里主要为了测试接口方便
        if (noLoginUrl && StrUtil.isBlank(token)) {
            return true;
        }
        Claims c = JwtUtil.parseToken(token, appConfig.getTokenSigningKey());
        if (JwtUtil.isNullOrExpired(c)) {
            // 如果免登陆接口携带的是过期的Token，这个时候直接返回给Controller即可。
            // 这样可以规避不必要的重新登录，而对于Controller，可以将本次请求视为未登录用户的请求。
            if (noLoginUrl) {
                return true;
            }
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            this.outputResponseMessage(response,
                    ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "用户会话已过期或尚未登录，请重新登录！"));
            return false;
        }
        String sessionId = (String) c.get("sessionId");
        String sessionIdKey = RedisKeyUtil.makeSessionIdKey(sessionId);
        RBucket<String> sessionData = redissonClient.getBucket(sessionIdKey);
        TokenData tokenData = null;
        if (sessionData.isExists()) {
            tokenData = JSON.parseObject(sessionData.get(), TokenData.class);
        }
        if (tokenData == null) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            this.outputResponseMessage(response,
                    ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "用户会话已失效，请重新登录！"));
            return false;
        }
        tokenData.setToken(token);
        TokenData.addToRequest(tokenData);
        // 如果url是免登陆、白名单中，则不需要进行鉴权操作
        //if (!noLoginUrl && Boolean.FALSE.equals(tokenData.getIsAdmin()) && !this.hasPermission(sessionId, url)) {
        //    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        //    this.outputResponseMessage(response, ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION));
        //    return false;
        //}
        if (JwtUtil.needToRefresh(c)) {
            String refreshedToken = JwtUtil.generateToken(c, appConfig.getExpiration(), appConfig.getTokenSigningKey());
            response.addHeader(appConfig.getRefreshedTokenHeaderKey(), refreshedToken);
        }
        return true;
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String token = request.getHeader(appConfig.getTokenHeaderKey());
        if (StrUtil.isBlank(token)) {
            token = request.getParameter(appConfig.getTokenHeaderKey());
        }
        if (StrUtil.isBlank(token)) {
            token = request.getHeader(ApplicationConstant.HTTP_HEADER_INTERNAL_TOKEN);
        }
        return token;
    }

    @SuppressWarnings("unchecked")
    private boolean hasPermission(String sessionId, String url) {
        // 为了提升效率，先检索Caffeine的一级缓存，如果不存在，再检索Redis的二级缓存，并将结果存入一级缓存。
        Set<String> localPermSet;
        String permKey = RedisKeyUtil.makeSessionPermIdKey(sessionId);
        Cache.ValueWrapper wrapper =
                cacheManager.getCache(CacheConfig.CacheEnum.USER_PERMISSION_CACHE.name()).get(permKey);
        if (wrapper == null) {
            RSet<String> permSet = redissonClient.getSet(permKey);
            localPermSet = new HashSet<>(permSet);
            cacheManager.getCache(CacheConfig.CacheEnum.USER_PERMISSION_CACHE.name()).put(permKey, localPermSet);
        } else {
            localPermSet = (Set<String>) wrapper.get();
        }
        return localPermSet.contains(url);
    }

    private boolean isNoAuthInterface(Object handler) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;
            return hm.getBeanType().getAnnotation(NoAuthInterface.class) != null
                    || hm.getMethodAnnotation(NoAuthInterface.class) != null;
        }
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        // 这里需要空注解，否则sonar会不happy。
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        // 这里需要空注解，否则sonar会不happy。
    }

    private void outputResponseMessage(HttpServletResponse response, ResponseResult<Object> respObj) {
        PrintWriter out;
        try {
            out = response.getWriter();
        } catch (IOException e) {
            log.error("Failed to call OutputResponseMessage.", e);
            return;
        }
        response.setContentType("application/json; charset=utf-8");
        out.print(JSON.toJSONString(respObj));
        out.flush();
        out.close();
    }
}
