package cn.wisenergy.shiro.filter;

import cn.wisenergy.common.utils.exception.BASE_RESP_CODE_ENUM;
import cn.wisenergy.common.utils.exception.Result;
import cn.wisenergy.model.app.WorkUser;
import cn.wisenergy.shiro.util.CustomerRealm;
import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionException;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @author: nh
 * @create: 2021-04-06 17:41
 **/
public class KickoutSessionControlFilter extends AccessControlFilter {
    protected Logger log = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private RedisTemplate redisTemplate;

    private SessionManager sessionManager;

    private CustomerRealm customerRealm;

    // false为提出之前登录的用户
    private boolean kickoutAfter = false;

    // 用一个账号最大的会话数量
    private int maxSession = 1;

    private static final String DEFAULT_KICKOUT_CACHE_KEY_PREFIX = "shiro:cache:kickout:";

    private String keyprefix = DEFAULT_KICKOUT_CACHE_KEY_PREFIX;

    private static final long EXPIRE_TIME = 30 * 60;

    public void setCustomerRealm(CustomerRealm customerRealm) {
        this.customerRealm = customerRealm;
    }

    String getRedisKickoutKey(Integer userId) {
        return keyprefix + userId;
    }

    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        //如果是小程序请求或请求登录接口，就放行
        return isMiniProgram((HttpServletRequest) request);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);

        HttpServletResponse httpServletResponse = null;
        // 没有登陆或没有设置“记住我”抛出未登录异常
        if (!subject.isAuthenticated() && !subject.isRemembered()) {
            // 没有登陆，抛出异常
            httpServletResponse = (HttpServletResponse) response;
            thrLogoutException(httpServletResponse, Result.RESULT_FLG.FAIL,BASE_RESP_CODE_ENUM.PLEASE_RE_LOGIN);
            return false;
        }

        /** 刷新redis缓存认证信息,存在redis中的认证信息k为登录时写入usernamePasswordToken的username,
            v为写入SimpleAuthenticationInfo中的principal **/
        WorkUser principal = (WorkUser) subject.getPrincipal();
        // 缓存管理
        CacheManager cacheManager = customerRealm.getCacheManager();
        // 缓存
        Cache<Object, Object> cache = cacheManager.getCache(customerRealm.getAuthenticationCacheName());
        // 覆盖，重新写入过期时间
        AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal,principal.getPassword(),customerRealm.getName());
        cache.put(principal.getLoginName(), authenticationInfo);

        // 账号PC端只允许一位同时在线登录，做踢出用户操作
        // 获取当前用户请求所携带的session
        Session session = subject.getSession();
        // 当前用户
        WorkUser user = (WorkUser) subject.getPrincipal();
        Integer userId = user.getId();

        // 获取用户登录账户的session队列,并将超过规定人数的用户进项标记
        this.getSessionQue(session, userId);

        // 如果当前用户被标记，则将用户登出
        if (session.getAttribute("kickout") != null) {
            try {
                subject.logout();
                // 当前拦截器为唯一拦截器，抛出未登录异常，结束本次请求
                httpServletResponse = (HttpServletResponse) response;
                thrLogoutException(httpServletResponse, Result.RESULT_FLG.FAIL,BASE_RESP_CODE_ENUM.PLEASE_RE_LOGIN);
                return false;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    // 获取用户登录账户的session队列，并将超过规定人数的用户进项标记
    public void getSessionQue(Session session, Integer userId) {
        Serializable sessionId = session.getId();

        // 初始化用户的队列放在缓存中
        Deque<Serializable> deque = (Deque<Serializable>) redisTemplate.opsForValue().get(getRedisKickoutKey(userId));
        if (deque == null || deque.size() == 0) {
            deque = new LinkedList<>();
        }
        // 队列中没有数据
        if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
            deque.push(sessionId);
        }
        // 队列数据数量大于设置的用户登录数量
        while (deque.size() > maxSession) {
            Serializable kickoutSessionId = null;
            // 为true踢出后面的登录者
            if (kickoutAfter) {
                kickoutSessionId = deque.removeFirst();
            } else {
                kickoutSessionId = deque.removeLast();
            }
            try {
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if (kickoutSession != null) {
                    // 设置标记为被踢出
                    kickoutSession.setAttribute("kickout", true);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 更新redis中的用户登录队列
        redisTemplate.opsForValue().set(getRedisKickoutKey(userId), deque, EXPIRE_TIME, TimeUnit.SECONDS);
    }

    // 将userId对应用户的所有登录session标记为踢出
    public void cleanUserLogin(Integer userId){
        Deque<Serializable> deque = (Deque<Serializable>) redisTemplate.opsForValue().get(getRedisKickoutKey(userId));
        if (deque != null && deque.size() != 0) {
            for (Serializable serializable : deque) {
                try {
                    Session session = sessionManager.getSession(new DefaultSessionKey(serializable));
                    if (session != null) {
                        session.setAttribute("kickout", true);
                    }
                } catch (SessionException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 请求是为小程序或请求接口为登录接口、小程序退出接口
    public boolean isMiniProgram(HttpServletRequest request) {
        boolean isMini = null != request.getHeader("identity") && request.getHeader("identity").equals("miniprogram");
        return isMini || request.getRequestURI().equals("/login") || request.getRequestURI().equals("/wxLogout");
    }

    // 抛出未登录异常
    public void thrLogoutException(HttpServletResponse response, Result.RESULT_FLG resultFlg, BASE_RESP_CODE_ENUM respCodeEnum){
        PrintWriter writer = null;
        try {
            Result result = new Result();
            result.setResult(resultFlg.getValue());
            if (respCodeEnum != null) {
                result.setErrorCode(respCodeEnum.getCode());
                result.setErrorMsg(respCodeEnum.getMsg());
            }
            response.setContentType("application/json; charset=UTF-8");
            writer = response.getWriter();
            writer.write(JSONObject.fromObject(result).toString());
            writer.flush();
        } catch (Exception e) {
            IOUtils.closeQuietly(writer);
            log.error("接口异常:{}", ExceptionUtils.getFullStackTrace(e));
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }

}
