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

import cn.reghao.jutil.jdk.security.RandomString;
import cn.reghao.jutil.jdk.security.RsaCryptor;
import cn.reghao.jutil.jdk.string.IDObfuscation;
import cn.reghao.tnb.account.api.constant.TokenType;
import cn.reghao.tnb.account.api.iface.AccountQuery;
import cn.reghao.tnb.account.app.db.mapper.LoginAttemptsMapper;
import cn.reghao.tnb.account.app.model.constant.LoginPlat;
import cn.reghao.tnb.account.api.dto.AccountInfo;
import cn.reghao.tnb.account.app.model.po.LoginAttempts;
import cn.reghao.tnb.account.app.service.AccountTokenService;
import cn.reghao.tnb.account.app.db.mapper.UserAccountMapper;
import cn.reghao.tnb.account.app.redis.ds.RedisOps;
import cn.reghao.jutil.web.ServletUtil;
import cn.reghao.tnb.account.app.model.dto.AccountToken;
import cn.reghao.tnb.account.app.security.form.AccountAuthToken;
import cn.reghao.tnb.account.app.service.PubkeyService;
import cn.reghao.tnb.account.app.util.JwtUtil;
import cn.reghao.tnb.account.app.redis.RedisKeys;
import cn.reghao.tnb.account.app.model.vo.RefreshPayload;
import cn.reghao.tnb.account.app.redis.ds.RedisString;
import cn.reghao.tnb.account.app.redis.ds.RedisStringObject;
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 javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.List;
import java.util.Set;

/**
 * @author reghao
 * @date 2023-02-16 14:57:49
 */
@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 UserAccountMapper userAccountMapper;
    private final LoginAttemptsMapper loginAttemptsMapper;
    private final PubkeyService pubkeyService;
    private final IDObfuscation idObfuscation;

    public AccountTokenServiceImpl(RedisOps redisOps, RedisString redisString, RedisStringObject redisStringObject,
                                   UserAccountMapper userAccountMapper, LoginAttemptsMapper loginAttemptsMapper,
                                   ServerProperties serverProperties, PubkeyService pubkeyService,
                                   IDObfuscation idObfuscation) {
        long sessionTimeout = serverProperties.getServlet().getSession().getTimeout().getSeconds();
        this.redisOps = redisOps;
        this.redisString = redisString;
        this.redisStringObject = redisStringObject;
        this.userAccountMapper = userAccountMapper;
        this.loginAttemptsMapper = loginAttemptsMapper;
        this.pubkeyService = pubkeyService;
        this.idObfuscation = idObfuscation;
    }

    @Override
    public AccountInfo getAccountInfo(int type, String userdata) {
        AccountAuthToken userAuthToken = getAuthToken(type, userdata);
        if (userAuthToken != null) {
            long userId = userAuthToken.getUserId();
            return userAccountMapper.findAccountInfo(userId);
        }

        return null;
    }

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

        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 userdata) {
        String redisKey = RedisKeys.getLoginSuccessKey(userdata);
        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 {
            AccountAuthToken userAuthToken = JwtUtil.getAuthentication1(token, rsaPublicKey, idObfuscation);
            if (userAuthToken != null) {
                return userAuthToken;
            }
            redisOps.del(savedSignKey);
        } catch (Exception ignore) {
            redisOps.del(savedSignKey);
        }

        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.parseRefreshToken(refreshToken, signKey);
        int plat = refreshPayload.getPlat();
        String loginId = refreshPayload.getLoginId();
        long userId = refreshPayload.getUserId();
        String userIdStr = idObfuscation.obfuscate(userId);
        Object object = redisStringObject.get(RedisKeys.getAuthTokenKey(userIdStr, plat, 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;
    }

    @Override
    public AccountToken grantUserToken(AccountAuthToken authToken) {
        long userId = authToken.getUserId();
        String userIdStr = idObfuscation.obfuscate(userId);
        int plat = authToken.getPlat();
        long accessExpireIn = 1000L*3600*24*7;
        long accessExpireAt = System.currentTimeMillis() + accessExpireIn;
        String accessSignKey = RandomString.getSalt(64);
        String accessToken = "";
        try {
            RSAPrivateKey privateKey = pubkeyService.getPrivateKey();
            accessToken = JwtUtil.createAccessToken(authToken, userIdStr, accessExpireAt, privateKey);
        } catch (Exception ignore) {
        }

        if (accessToken.isBlank()) {
            accessToken = JwtUtil.createAccessToken(authToken, userIdStr, accessExpireAt, accessSignKey);
        }

        long refreshExpireIn = accessExpireIn*4;
        long refreshExpireAt = System.currentTimeMillis() + refreshExpireIn;
        String refreshSignKey = RandomString.getSalt(64);
        String refreshToken = JwtUtil.createRefreshToken(authToken, refreshExpireAt, refreshSignKey);

        redisString.setWithTimeout(RedisKeys.getAccessSignKeyKey(accessToken), accessSignKey, accessExpireIn);
        redisString.setWithTimeout(RedisKeys.getRefreshSignKeyKey(refreshToken), refreshSignKey, refreshExpireIn);

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

    @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);

        String loginSuccessKey = RedisKeys.getLoginSuccessKey(userId, plat, loginId);
        if (timeout == 0) {
            timeout = 3600*24*30;
        }
        redisStringObject.setWithTimeout(loginSuccessKey, authToken, timeout);
    }

    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;
    }

    private void deleteSession(long userId, int plat, String loginId, boolean deleteSession) {
        if (LoginPlat.android.getValue() == plat) {
            revokeUserToken(userId, plat, loginId);
        }

        String userdata = String.format("%s:%s:%s", userId, plat, loginId);
        deleteLoginData(userdata);
        if (deleteSession) {
            deleteRedisSession(loginId);
        }
    }

    @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.web.getValue() == plat) {
                clearCookie();
            } else if (LoginPlat.android.getValue() == plat) {
                revokeUserToken(userId, plat, loginId);
            }

            String userdata = String.format("%s:%s:%s", userId, plat, loginId);
            deleteLoginData(userdata);
            deleteRedisSession(loginId);

            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);
    }

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

    private void deleteLoginData(String userdata) {
        String redisKey = RedisKeys.getLoginSuccessKey(userdata);
        redisOps.del(redisKey);
    }

    private void deleteRedisSession(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(String loginId) {
        LoginAttempts loginAttempts = loginAttemptsMapper.findByLoginId(loginId);
        if (loginAttempts != null && !loginAttempts.getRememberMe()) {
            long userId = loginAttempts.getUserId();
            int plat = loginAttempts.getPlat();
            deleteSession(userId, plat, loginId, true);
        }
    }

    @Override
    public void logout(long userId) {
        String keyPattern = "login:" + userId;
        Set<String> loginKeys = redisOps.keys(keyPattern);
        redisOps.del(loginKeys.toArray(new String[0]));
    }

    @Override
    public void deleteOnSessionExpired(String loginId) {
        /*LoginAttempts loginAttempts = loginAttemptsMapper.findByLoginId(loginId);
        if (loginAttempts != null && !loginAttempts.getRememberMe()) {
            long userId = loginAttempts.getUserId();
            int plat = loginAttempts.getPlat();
            deleteSession(userId, plat, loginId, false);
        }*/
    }
}
