package com.tzld.piaoquan.api.config;

import com.alibaba.fastjson.JSON;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.tzld.piaoquan.api.annotation.JwtIgnore;
import com.tzld.piaoquan.api.annotation.RequireSign;
import com.tzld.piaoquan.api.common.enums.ExceptionEnum;
import com.tzld.piaoquan.api.common.enums.contentplatform.ContentPlatformAccountTypeEnum;
import com.tzld.piaoquan.api.common.exception.CommonException;
import com.tzld.piaoquan.api.model.config.LoginUserContext;
import com.tzld.piaoquan.api.model.po.contentplatform.ContentPlatformAccount;
import com.tzld.piaoquan.api.service.contentplatform.ContentPlatformAccountService;
import com.tzld.piaoquan.growth.common.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
@ConfigurationProperties(prefix = "author.interceptor")
public class JwtInterceptor implements HandlerInterceptor {

    ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 4,
            0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100000),
            new ThreadFactoryBuilder().setNameFormat("login-service-pool-%d").build(), new ThreadPoolExecutor.AbortPolicy());

    private static final String SLASH = "/";
    public static String TOKEN_PREFIX = "login.{token}";
    private static final String TOKEN_TEL_PREFIX = "login.token.tel.";
    private static final String TOKEN_LIST_PREFIX = "login.account.token.list.";
    private static final long TOKEN_INDEX_EXPIRE_TIME = RedisUtils.DEFAULT_EXPIRE_TIME + 24 * 60 * 60L;
    private Set<String> excludePaths;
    private Set<String> excludeUris;

    /**
     * 内部管理平台调用时携带的鉴权 Key，用于绕过 JWT 校验
     */
    private String internalKey;

    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    private ContentPlatformAccountService accountService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (HttpMethod.OPTIONS.name().equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.setHeader("Access-Control-Allow-Methods", "*");
            response.setHeader("Access-Control-Allow-Headers", "*");
            response.setHeader("Access-Control-Max-Age", "3600");
            return true;
        }

        // 忽略带JwtIgnore注解的请求, 不做后续token认证校验
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
            if (jwtIgnore != null) {
                return Boolean.TRUE;
            }
        }
        String uri = request.getServletPath();
        if (this.exclude(uri)) {
            //放行excludePaths包含的目录路径和excludeUris包含的URI
            return true;
        }
        // 管理平台内部调用：通过 X-Internal-Key 鉴权，绕过 JWT 校验
        if (StringUtils.isNotBlank(internalKey) && uri.startsWith("/contentPlatform/account/")) {
            String requestInternalKey = request.getHeader("X-Internal-Key");
            if (internalKey.equals(requestInternalKey)) {
                return Boolean.TRUE;
            }
        }
        final String authHeader = request.getHeader("token");
        if (StringUtils.isBlank(authHeader)) {
            throw new CommonException(ExceptionEnum.Not_LOGIN);
        }

        validToken(authHeader);

        // 标记了 @RequireSign 的接口需要进行防重放校验
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            RequireSign requireSign = handlerMethod.getMethodAnnotation(RequireSign.class);
            if (requireSign != null) {
                validateSign(request, authHeader);
            }
        }

        // account 管理接口仅允许内部账号访问
        if (uri.startsWith("/contentPlatform/account/")) {
            ContentPlatformAccount loginUser = LoginUserContext.getUser();
            if (loginUser == null || loginUser.getType() != ContentPlatformAccountTypeEnum.INTERNAL.getVal()) {
                throw new CommonException(ExceptionEnum.Not_LOGIN);
            }
        }

        return Boolean.TRUE;
    }


    /**
     * 校验Token
     */
    private void validToken(String authHeader) {
        String key = TOKEN_PREFIX.replace("{token}", authHeader);
        String loginUserString = redisUtils.getString(key);
        if (StringUtils.isBlank(loginUserString)) {
            cleanExpiredTokenAsync(authHeader);
            throw new CommonException(ExceptionEnum.Not_LOGIN);
        }
        ContentPlatformAccount account;
        try {
            account = JSON.parseObject(loginUserString, ContentPlatformAccount.class);
        } catch (Exception e) {
            throw new CommonException(ExceptionEnum.Not_LOGIN);
        }
        if (Objects.isNull(account)) {
            throw new CommonException(ExceptionEnum.Not_LOGIN);
        }
        // token验证完成后获取对应信息，存放在本地线程中
        LoginUserContext.setLoginUser(account);
        // 异步处理续期
        renewalToken(authHeader);
    }

    private boolean exclude(String uri) {
        if (SLASH.equals(uri)) {
            return true;
        }
        // 仅校验内容平台接口
        if (!uri.startsWith("/contentPlatform")) {
            return true;
        }
        for (String path : excludePaths) {
            if (uri.startsWith(path)) {
                return true;
            }
        }
        return excludeUris.contains(uri);
    }

    public Set<String> getExcludePaths() {
        return excludePaths;
    }

    public void setExcludePaths(Set<String> excludePaths) {
        this.excludePaths = excludePaths;
    }

    public Set<String> getExcludeUris() {
        return excludeUris;
    }

    public void setExcludeUris(Set<String> excludeUris) {
        this.excludeUris = excludeUris;
    }

    public String getInternalKey() {
        return internalKey;
    }

    public void setInternalKey(String internalKey) {
        this.internalKey = internalKey;
    }

    /**
     * 请求防重放校验：timestamp 必须在 ±5 分钟内，同一 token 的 nonce 只能使用一次。
     */
    private void validateSign(HttpServletRequest request, String token) {
        String timestamp = request.getHeader("X-Timestamp");
        String nonce = request.getHeader("X-Nonce");

        if (StringUtils.isBlank(timestamp) || StringUtils.isBlank(nonce)) {
            throw new CommonException(ExceptionEnum.PARAM_ERROR.getCode(), "请求缺少签名参数");
        }

        // 校验时间戳（±5分钟）
        long requestTime;
        try {
            requestTime = Long.parseLong(timestamp);
        } catch (NumberFormatException e) {
            throw new CommonException(ExceptionEnum.PARAM_ERROR.getCode(), "时间戳格式错误");
        }
        long now = System.currentTimeMillis();
        long allowedSkew = 5 * 60 * 1000L;
        if (requestTime < now - allowedSkew || requestTime > now + allowedSkew) {
            throw new CommonException(ExceptionEnum.PARAM_ERROR.getCode(), "请求已过期");
        }

        // 防重放：nonce 只能使用一次
        String nonceKey = "sign_nonce:" + token + ":" + nonce;
        boolean success = redisUtils.tryLock(nonceKey, "1", 10 * 60L);
        if (!success) {
            log.warn("Replay attack detected - token={}, nonce={}", token, nonce);
            throw new CommonException(ExceptionEnum.PARAM_ERROR.getCode(), "请求不能重复提交");
        }

    }

    private void renewalToken(String token) {
        executeAsync(() -> {
            try {
                String key = TOKEN_PREFIX.replace("{token}", token);
                String loginUserString = redisUtils.getString(key);
                if (StringUtils.isBlank(loginUserString)) {
                    // token 已过期，从列表中清理
                    cleanExpiredToken(token);
                    return;
                }
                ContentPlatformAccount account = JSON.parseObject(loginUserString, ContentPlatformAccount.class);
                Long seconds = redisUtils.getKeyExpire(key);
                if (seconds < 24 * 60 * 60) {
                    // 续期时重新从数据库获取最新账户信息并更新缓存
                    if (Objects.nonNull(account) && Objects.nonNull(account.getId())) {
                        ContentPlatformAccount latestAccount = accountService.getAccountById(account.getId());
                        if (Objects.nonNull(latestAccount)) {
                            boolean renewed = redisUtils.setValueWithExpireIfPresent(key,
                                    JSON.toJSONString(latestAccount), RedisUtils.DEFAULT_EXPIRE_TIME);
                            if (renewed && StringUtils.isNotBlank(latestAccount.getTelNum())) {
                                redisUtils.setValueWithExpire(TOKEN_TEL_PREFIX + token,
                                        latestAccount.getTelNum(), TOKEN_INDEX_EXPIRE_TIME);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // 续期失败不影响正常请求，仅记录日志
                log.error("renewalToken error, token={}", token, e);
            }
        }, "renewalToken", token);
    }

    private void cleanExpiredTokenAsync(String token) {
        executeAsync(() -> cleanExpiredToken(token), "cleanExpiredToken", token);
    }

    private void executeAsync(Runnable task, String taskName, String token) {
        try {
            executor.execute(task);
        } catch (RejectedExecutionException e) {
            // 异步维护失败不能改变本次请求的鉴权结果
            log.error("{} rejected, token={}", taskName, token, e);
        }
    }

    /**
     * 触发清理：遍历该手机号的所有 token，移除已过期的
     */
    private void cleanExpiredToken(String token) {
        try {
            String telKey = TOKEN_TEL_PREFIX + token;
            String telNum = redisUtils.getString(telKey);
            if (StringUtils.isBlank(telNum)) {
                redisUtils.del(telKey);
                return;
            }
            String listKey = TOKEN_LIST_PREFIX + telNum;
            List<String> tokens = redisUtils.listRange(listKey);
            if (tokens == null || tokens.isEmpty()) {
                redisUtils.del(telKey);
                return;
            }
            for (String t : tokens) {
                if (StringUtils.isBlank(t)) {
                    continue;
                }
                String loginKey = TOKEN_PREFIX.replace("{token}", t);
                if (StringUtils.isBlank(redisUtils.getString(loginKey))) {
                    // token 已过期，从列表和映射中移除
                    redisUtils.listRemove(listKey, t);
                    redisUtils.del(TOKEN_TEL_PREFIX + t);
                    redisUtils.del("sign_key." + t);
                }
            }
        } catch (Exception e) {
            log.error("cleanExpiredToken error, token={}", token, e);
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception
            ex) throws Exception {
        LoginUserContext.remove();
    }
}
