package ${vhGrpcBuilder_packageName}.${vhGrpcBuilder_sprojectName}.util;

import com.viewhigh.vhsc.support.RedisKey;
import com.viewhigh.vhsc.support.util.ObjectUtils;
import ${vhGrpcBuilder_packageName}.${vhGrpcBuilder_sprojectName}.vo.user.UserVo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Component
public class TokenUtil implements Serializable {

    private static final long serialVersionUID = -3301605591108950415L;

    private final transient Logger logger = LoggerFactory.getLogger(this.getClass());

    private static final BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(500);
    private final transient ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 3, 2, TimeUnit.MINUTES, blockingQueue, new ThreadPoolExecutor.DiscardPolicy());
    public static final String CLAIM_KEY_CREATED = "created";

    public static final String CLAIM_KEY_ID = "id"; //供应商用户标识
    public static final String CLAIM_KEY_NAME = "name";//供应商用户名称
    public static final String CLAIM_KEY_SID = "sid"; //供应商标识
    public static final String CLAIM_KEY_SNAME = "sname";//供应商名称

    public static final String CLAIM_KEY_ADMIN = "admin";//用户所属角色
    public static final String CLAIM_KEY_STATUS = "status";//用户所属角色

    private static final String ID_HASH_KEY = "id";
    private static final String USER_HASH_KEY = "user";
    private static final String TOKEN_HASH_KEY = "token";


    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;

    @Autowired
    private transient StringRedisTemplate strRedisTemplate;
    @Autowired
    private transient RedisTemplate<String, Object> redisTemplate;

    public UserVo getUserFromToken(String token) {
        UserVo user = null;
        //打印临时策略更改
        Claims claims = getClaimsFromToken(token);
        if (claims != null && claims.containsKey(CLAIM_KEY_ID)) {
            Long id = Long.valueOf("" + claims.get(CLAIM_KEY_ID));
            String name = claims.get(CLAIM_KEY_NAME, String.class);
            Long sid = Long.valueOf("" + claims.get(CLAIM_KEY_SID));

            String sname = claims.get(CLAIM_KEY_SNAME, String.class);
            Integer admin = claims.get(CLAIM_KEY_ADMIN, Integer.class);
            Integer status = claims.get(CLAIM_KEY_STATUS, Integer.class);
            user = new UserVo();
            user.setId(id);
            user.setName(name);
            user.setSid(sid);
            user.setSname(sname);
            user.setAdmin(admin);
            user.setStatus(status);
        }
        return user;
    }

    public String getUnameFromToken(String token) {
        String uname = null;
        try {
            final Claims claims = getClaimsFromToken(token);
            if (claims != null) {
                uname = claims.get(CLAIM_KEY_NAME, String.class);
            }
        } catch (Exception ex) {
            uname = null;
            logger.error("error when get uname from token {}, uname is null.", token, ex);
        }
        return uname;
    }

    public Long getUidFromToken(String token) {
        Long uid = -1l;
        try {
            final Claims claims = getClaimsFromToken(token);
            if (claims != null) {
                uid = Long.valueOf(claims.get(CLAIM_KEY_ID).toString());
            }
        } catch (Exception ex) {
            uid = -1l;
            logger.error("error when get uid from token {}, uid is -1", token, ex);
        }
        return uid;
    }

    public Long getSidFromToken(String token) {
        Long sid = -1l;
        try {
            final Claims claims = getClaimsFromToken(token);
            if (claims != null) {
                sid = Long.valueOf(claims.get(CLAIM_KEY_SID).toString());
            }
        } catch (Exception ex) {
            sid = -1l;
            logger.error("error when get sid from token {}, sid is -1", token, ex);
        }
        return sid;
    }

    public String getSnameFromToken(String token) {
        String sname = null;
        try {
            final Claims claims = getClaimsFromToken(token);
            if (claims != null) {
                sname = claims.get(CLAIM_KEY_SNAME, String.class);
            }
        } catch (Exception ex) {
            sname = null;
            logger.error("error when get sname from token {}, sname is null", token, ex);
        }
        return sname;
    }

    public Date getCreatedDateFromToken(String token) {
        Date created = null;
        try {
            final Claims claims = getClaimsFromToken(token);
            if (claims != null) {
                created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
            }
        } catch (Exception e) {
            created = null;
            logger.error("error when get CreatedDate from token {}, sname is null", token, e);
        }
        return created;
    }

    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            if (token.equals("printToken")) {
                claims=null;
            } else {
                claims = Jwts.parser()
                        .setSigningKey(secret)
                        .parseClaimsJws(token)
                        .getBody();
            }
        } catch (Exception ex) {
            claims = null;
            logger.error("error when get cliams from token {} ", token, ex);
        }
        return claims;
    }

    public String generateToken(UserVo user) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_NAME, user.getName());
        claims.put(CLAIM_KEY_CREATED, new Date());
        claims.put(CLAIM_KEY_ID, user.getId());
        claims.put(CLAIM_KEY_SID, user.getSid());
        claims.put(CLAIM_KEY_SNAME, user.getSname());
        claims.put(CLAIM_KEY_ADMIN, user.getAdmin());
        claims.put(CLAIM_KEY_STATUS, user.getStatus());
        String token = generateToken(claims);
        // 同步设置 token ttl
        Long uid = user.getId();
        Map<String, Object> map = new HashMap<>();
        map.put(ID_HASH_KEY, uid);
        map.put(USER_HASH_KEY, user);
        map.put(TOKEN_HASH_KEY, token);
        redisTemplate.opsForHash().putAll(RedisKey.USER_KEY_PREFIX + uid, map);
        redisTemplate.expire(RedisKey.USER_KEY_PREFIX + uid, expiration, TimeUnit.SECONDS);

        return token;
    }

    public String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
//                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 刷新token失效时间，并不新生成token
     *
     * @param token
     */
    public void refreshToken(String token) {
        threadPoolExecutor.submit(new RefreshTokenDelegator(token));
    }


    private final class RefreshTokenDelegator implements Runnable {
        private String token;

        public RefreshTokenDelegator(final String token) {
            this.token = token;
        }

        public void run() {
            try {
                if (isTokenValid(token)) {
                    Long uid = getUidFromToken(token);
                    strRedisTemplate.expire(RedisKey.USER_KEY_PREFIX + uid, expiration, TimeUnit.SECONDS);
                    logger.info("token {}", token);
                    logger.info("token expireation {}", expiration);
                    logger.info("token size {}", blockingQueue.size());
                } else {
                    logger.warn("token is invalid, can not refresh.");
                }
            } catch (Exception ex) {
                logger.error("refresh token {} error.", ex);
            }
        }
    }

    public Boolean isTokenValid(String token) {
        if (StringUtils.isEmpty(token))
            return false;
//    	1. 可parse
        Long uid = getUidFromToken(token);
        if (uid == -1)
            return false;
//    	2. uid - token 映射关系正确 (登录校验完成，记录token-uid映射关系)
//    	3. token 未失效
        String tokenInCache = getTokenFromCache(uid);
        return ObjectUtils.isEquals(token, tokenInCache);
    }

    private String getTokenFromCache(Long uid) {
        Object token = redisTemplate.opsForHash().get(RedisKey.USER_KEY_PREFIX + uid, TOKEN_HASH_KEY);
        if (ObjectUtils.isEmpty(token)) {
            return "";
        }
        return (String) token;
    }

    public void destroyToken(String token) {
        Long uid = getUidFromToken(token);
        redisTemplate.delete(RedisKey.USER_KEY_PREFIX + uid);
    }
}