package cn.reghao.tnb.auth.app.service.impl;

import cn.reghao.jutil.jdk.security.RandomString;
import cn.reghao.jutil.jdk.security.RsaCryptor;
import cn.reghao.tnb.auth.api.constant.TokenType;
import cn.reghao.tnb.auth.app.db.mapper.UserLoginMapper;
import cn.reghao.tnb.auth.app.model.constant.LoginPlat;
import cn.reghao.tnb.auth.app.model.po.UserLogin;
import cn.reghao.tnb.auth.app.service.AccountTokenService;
import cn.reghao.tnb.auth.app.config.redis.ds.RedisOps;
import cn.reghao.tnb.common.web.ServletUtil;
import cn.reghao.tnb.auth.app.model.vo.AccountToken;
import cn.reghao.tnb.auth.app.security.form.AccountAuthToken;
import cn.reghao.tnb.auth.app.service.PubkeyService;
import cn.reghao.tnb.auth.app.security.AuthConverter;
import cn.reghao.tnb.auth.JwtUtil;
import cn.reghao.tnb.auth.app.config.redis.RedisKeys;
import cn.reghao.tnb.auth.model.RefreshPayload;
import cn.reghao.tnb.auth.app.config.redis.ds.RedisString;
import cn.reghao.tnb.auth.app.config.redis.ds.RedisStringObject;
import cn.reghao.tnb.auth.model.JwtPayload;
import cn.reghao.tnb.common.util.ConstantId;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.stereotype.Service;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @author reghao
 * @date 2023-02-16 14:57:49
 */
@Slf4j
@Service
public class AccountTokenServiceImpl implements AccountTokenService {
    private final String cookieName = "USERDATA";
    private final String domain = "reghao.cn";

    private final RedisOps redisOps;
    private final RedisString redisString;
    private final RedisStringObject redisStringObject;
    private final UserLoginMapper userLoginMapper;
    private final PubkeyService pubkeyService;

    public AccountTokenServiceImpl(RedisOps redisOps, RedisString redisString, RedisStringObject redisStringObject,
                                   UserLoginMapper userLoginMapper, ServerProperties serverProperties,
                                   PubkeyService pubkeyService) {
        long sessionTimeout = serverProperties.getServlet().getSession().getTimeout().getSeconds();
        this.redisOps = redisOps;
        this.redisString = redisString;
        this.redisStringObject = redisStringObject;
        this.userLoginMapper = userLoginMapper;
        this.pubkeyService = pubkeyService;
    }

    @Override
    public AccountToken grantUserToken(AccountAuthToken authToken) {
        String signKeyRandom = RandomString.getSalt(64);
        RSAPrivateKey signKeyPrivate = null;
        try {
            signKeyPrivate = pubkeyService.getPrivateKey();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        long userId = authToken.getUserId();
        long accessExpireIn = 1000L*3600*24*7;
        long accessExpireAt = System.currentTimeMillis() + accessExpireIn;
        String accessToken = "";
        JwtPayload jwtPayload = AuthConverter.getJwtPayload(authToken);
        if (signKeyPrivate != null) {
            accessToken = JwtUtil.createAccessToken(jwtPayload, accessExpireAt, signKeyPrivate);
        } else {
            accessToken = JwtUtil.createAccessToken(jwtPayload, accessExpireAt, signKeyRandom);
        }

        long refreshExpireIn = accessExpireIn*4;
        long refreshExpireAt = System.currentTimeMillis() + refreshExpireIn;
        String refreshToken = JwtUtil.createRefreshToken(jwtPayload, refreshExpireAt, signKeyRandom);

        if (signKeyPrivate == null) {
            redisString.setWithTimeout(RedisKeys.getAccessSignKeyKey(accessToken), signKeyRandom, accessExpireIn);
        }
        redisString.setWithTimeout(RedisKeys.getRefreshSignKeyKey(refreshToken), signKeyRandom, refreshExpireIn);

        String loginId = authToken.getLoginId();
        redisStringObject.setWithTimeout(RedisKeys.getAuthTokenKey(userId, loginId), authToken, accessExpireIn);
        redisString.setWithTimeout(RedisKeys.getAccessTokenKey(userId, loginId), accessToken, accessExpireIn);
        redisString.setWithTimeout(RedisKeys.getRefreshTokenKey(userId, loginId), refreshToken, refreshExpireIn);
        return new AccountToken(accessToken, accessExpireAt, refreshToken, refreshExpireAt);
    }

    private JwtPayload getJwtPayload(AccountAuthToken accountAuthToken, String signKey, PrivateKey privateKey) {
        return null;
    }

    public void refreshToken(long userId, String loginId) {
        log.info("令牌刷新尚未实现");
    }

    @Override
    public void setCookie(AccountAuthToken authToken, long timeout) {
        long userId = authToken.getUserId();
        int plat = authToken.getPlat();
        String loginId = authToken.getLoginId();

        String userdata = String.format("%s:%s:%s", userId, plat, loginId);
        Cookie cookie3 = generateCookie(cookieName, userdata, timeout);
        ServletUtil.getResponse().addCookie(cookie3);
    }

    private Cookie generateCookie(String name, String value, long timeout) {
        String path = "/";

        Cookie cookie = new Cookie(name, value);
        if (timeout != 0) {
            cookie.setMaxAge((int) timeout);
        }
        cookie.setDomain(domain);
        //cookie.setSecure(true);
        cookie.setSecure(false);
        cookie.setPath(path);
        return cookie;
    }

    @Override
    public void logout() {
        // 删除 cookie 中的数据(或者是撤销 token)
        // 删除 redis 中的 account login 数据
        // 删除 redis 中的 session 数据
        // 删除 SecurityContextHolder 中的数据
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication instanceof AccountAuthToken) {
            AccountAuthToken authToken = (AccountAuthToken) authentication;
            long userId = authToken.getUserId();
            int plat = authToken.getPlat();
            String loginId = authToken.getLoginId();
            if (LoginPlat.rest.getValue() == plat) {
                revokeUserToken(userId, loginId);
            } else {
                clearCookie();
                deleteSession(loginId);
            }

            clearSecurityContext();
        }
    }

    private void clearSecurityContext() {
        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(null);
        SecurityContextHolder.clearContext();
    }

    @Override
    public void clearCookie() {
        String path = "/";
        Cookie cookie2 = new Cookie(cookieName, null);
        cookie2.setMaxAge(0);
        cookie2.setDomain(domain);
        cookie2.setSecure(true);
        cookie2.setPath(path);
        ServletUtil.getResponse().addCookie(cookie2);
    }

    @Override
    public void logout(String loginId) {
        List<UserLogin> userLoginList = userLoginMapper.findByLoginIds(List.of(loginId));
        if (!userLoginList.isEmpty() && !userLoginList.get(0).getRememberMe()) {
            UserLogin userLogin = userLoginList.get(0);
            long userId = userLogin.getUserId();
            int plat = userLogin.getPlat();
            if (LoginPlat.rest.getValue() == plat) {
                revokeUserToken(userId, loginId);
            } else {
                String userdata = String.format("%s:%s:%s", userId, plat, loginId);
                deleteSession(loginId);
            }

            AccountAuthToken authToken = getAuthToken();
            if (authToken != null && loginId.equals(authToken.getLoginId())) {
                clearSecurityContext();
            }
        }
    }

    private void revokeUserToken(long userId, String loginId) {
        String accessToken = redisString.get(RedisKeys.getAccessTokenKey(userId, loginId));
        String refreshToken = redisString.get(RedisKeys.getRefreshTokenKey(userId, loginId));
        String[] keys = List.of(
                        RedisKeys.getAuthTokenKey(userId, loginId),
                        RedisKeys.getAccessTokenKey(userId, loginId),
                        RedisKeys.getRefreshTokenKey(userId, loginId),
                        RedisKeys.getAccessSignKeyKey(accessToken),
                        RedisKeys.getRefreshSignKeyKey(refreshToken))
                .toArray(new String[0]);
        redisOps.del(keys);
    }

    private void deleteSession(String loginId) {
        HttpServletRequest request = ServletUtil.getRequest();
        if (request != null) {
            Object object = request.getAttribute(SessionRepositoryFilter.SESSION_REPOSITORY_ATTR);
            if (object instanceof RedisIndexedSessionRepository) {
                RedisIndexedSessionRepository sessionRepository = (RedisIndexedSessionRepository) object;
                sessionRepository.deleteById(loginId);
            }
        }
    }

    @Override
    public void logout(long userId) {
        String keyPattern = RedisKeys.getLoginSuccessKeyPattern(userId);
        List<String> loginIds = redisOps.keys(keyPattern).stream()
                .filter(key -> key.endsWith("auth_token"))
                .map(key -> {
                    String[] arr = key.split(":");
                    if (arr.length != 7) {
                        return null;
                    }

                    return arr[arr.length-2];
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        if (!loginIds.isEmpty()) {
            loginIds.forEach(this::logout);
        }

        long loginUser = getAuthToken().getUserId();
        if (loginUser == userId) {
            clearSecurityContext();
        }
    }

    @Override
    public void deleteOnSessionExpired(String loginId) {
    }

    @Override
    public long getUserId(int type, String userdata) {
        AccountAuthToken userAuthToken = getAuthToken(type, userdata);
        return userAuthToken != null ? userAuthToken.getUserId() : ConstantId.ANONYMOUS_USER_ID;
    }

    @Override
    public Authentication getAuthentication() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication;
    }

    @Override
    public AccountAuthToken getAuthToken() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication instanceof AccountAuthToken) {
            return (AccountAuthToken) SecurityContextHolder.getContext().getAuthentication();
        }

        return null;
    }

    @Override
    public AccountAuthToken getAuthToken(int type, String userdata) {
        AccountAuthToken authToken = null;
        if (type == TokenType.cookie.getValue()) {
            authToken = getFromCookie(userdata);
        } else if (type == TokenType.token.getValue()) {
            authToken = getFromToken(userdata);
        }

        return authToken;
    }

    private AccountAuthToken getFromCookie(String loginId) {
        long userId = 0;
        String redisKey = RedisKeys.getAuthTokenKey(userId, loginId);
        Object object = redisStringObject.get(redisKey);
        if (object != null) {
            return (AccountAuthToken) object;
        }

        return null;
    }

    private AccountAuthToken getFromToken(String token) {
        String savedSignKey = redisString.get(RedisKeys.getJwtSignKey("pubkey"));
        RSAPublicKey rsaPublicKey = RsaCryptor.getRSAPublicKey(savedSignKey);
        try {
            JwtPayload jwtPayload = JwtUtil.getJwtPayload(token, rsaPublicKey);
            if (jwtPayload != null) {
                AccountAuthToken userAuthToken = AuthConverter.getAccountAuthToken(jwtPayload);
                long userId = userAuthToken.getUserId();
                String loginId = userAuthToken.getLoginId();
                String redisKey = RedisKeys.getAuthTokenKey(userId, loginId);
                boolean online = redisOps.exists(redisKey);
                return online ? userAuthToken : null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    @Override
    public AccountToken getUserToken(String refreshToken) throws Exception {
        String signKey = redisString.get(RedisKeys.getRefreshSignKeyKey(refreshToken));
        if (signKey == null) {
            throw new Exception("refresh-token 已过期");
        }

        RefreshPayload refreshPayload = JwtUtil.getRefreshPayload(refreshToken, signKey);
        int plat = refreshPayload.getPlat();
        String loginId = refreshPayload.getLoginId();
        long userId = refreshPayload.getUserId();
        Object object = redisStringObject.get(RedisKeys.getAuthTokenKey(userId, loginId));
        AccountAuthToken authToken = (AccountAuthToken) object;

        AccountToken accountToken = grantUserToken(authToken);
        String accessToken = ServletUtil.getBearerToken();
        if (accessToken != null) {
            redisOps.del(RedisKeys.getAccessSignKeyKey(accessToken));
        }
        redisOps.del(RedisKeys.getRefreshSignKeyKey(refreshToken));
        return accountToken;
    }
}
