package com.jy.system.service.impl;

import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.date.BetweenFormatter;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.jy.framework.config.JyConfig;
import com.jy.framework.exception.ServiceException;
import com.jy.framework.model.base.BaseIdPo;
import com.jy.framework.model.constants.CommonConstant;
import com.jy.framework.model.constants.DataScopeConstant;
import com.jy.framework.model.constants.MenuConstant;
import com.jy.framework.model.constants.SaSessionKey;
import com.jy.framework.model.constants.UserType;
import com.jy.framework.model.event.AccessEvent;
import com.jy.framework.redis.RedisCache;
import com.jy.framework.redis.RedisKey;
import com.jy.framework.satoken.AccessInfo;
import com.jy.framework.satoken.LoginContext;
import com.jy.framework.satoken.UserInfo;
import com.jy.framework.utils.AddressUtils;
import com.jy.framework.utils.AssertUtil;
import com.jy.framework.utils.ServletUtil;
import com.jy.system.dao.SysDeptDao;
import com.jy.system.dao.SysRoleDeptDao;
import com.jy.system.dao.SysUserDao;
import com.jy.system.model.dto.EditPwdDto;
import com.jy.system.model.dto.LoginDto;
import com.jy.system.model.dto.UserInfoDto;
import com.jy.system.model.entity.SysMenu;
import com.jy.system.model.entity.SysRole;
import com.jy.system.model.entity.SysUser;
import com.jy.system.model.vo.CaptchaImageVo;
import com.jy.system.model.vo.LoginInfoVo;
import com.jy.system.model.vo.MetaVo;
import com.jy.system.model.vo.RouterVo;
import com.jy.system.model.vo.SysMenuVo;
import com.jy.system.model.vo.UserInfoVo;
import com.jy.system.service.AuthService;
import com.jy.system.service.SysMenuService;
import com.jy.system.service.SysRoleService;
import com.wf.captcha.SpecCaptcha;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.springframework.util.FastByteArrayOutputStream;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Service
public class AuthServiceImpl implements AuthService {

    @Resource
    private ApplicationContext applicationContext;

    @Resource
    private HttpServletRequest request;

    @Resource
    private JyConfig jyConfig;

    @Resource
    private SysUserDao sysUserDao;

    @Resource
    private SysDeptDao sysDeptDao;

    @Resource
    private SysRoleDeptDao sysRoleDeptDao;

    @Resource
    private SysRoleService sysRoleService;

    @Resource
    private SysMenuService sysMenuService;

    @Override
    public CaptchaImageVo getCaptchaImage() {
        CaptchaImageVo vo = new CaptchaImageVo();

        // 是否开启验证码登录校验
        Boolean captchaOnOff = jyConfig.getCaptchaOnOff();
        vo.setCaptchaOnOff(captchaOnOff);

        // 未开启则跳过
        if (!captchaOnOff) {
            return vo;
        }

        // 生成验证码
        SpecCaptcha specCaptcha = new SpecCaptcha(150, 50, 5);
        String captchaValue = specCaptcha.text().toLowerCase();

        // 缓存验证码信息
        String captchaKey = IdWorker.getIdStr();
        String redisKey = RedisKey.getCaptchaCode(captchaKey);
        RedisCache.set(redisKey, captchaValue, Duration.ofMinutes(jyConfig.getCaptchaExpireTime()));

        // 转换流信息写出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        specCaptcha.out(os);

        // 赋值参数
        vo.setUuid(captchaKey);
        vo.setImg(Base64.encode(os.toByteArray()));

        // 关闭输出流
        IoUtil.close(os);

        return vo;
    }

    @Override
    public LoginInfoVo login(LoginDto dto) {
        // 去除前后空格
        dto.setUsername(dto.getUsername().trim());

        // 获取访问信息
        AccessInfo accessInfo = createAccessInfo();

        // 用户信息
        SysUser sysUser;

        try {

            // 账号是否被锁定
            validateLock(dto);

            // 验证码是否正确
            validateCaptcha(dto);

            // 获取用户信息
            sysUser = sysUserDao.getByUsername(dto.getUsername());

            // 验证用户
            validateUser(sysUser, dto);

        } catch (ServiceException e) {

            // 记录登录失败日志
            recordAccessLog(accessInfo, dto.getUsername(), false, e.getMessage());

            throw e;
        }

        // 清空用户密码错误次数
        clearUserLockCount(sysUser.getUsername());

        // 记录最后登录时间与登录ip
        sysUserDao.updateLoginInfo(sysUser.getId(), accessInfo.getIp(), accessInfo.getAccessTime());

        // 记录登录成功日志
        recordAccessLog(accessInfo, sysUser.getUsername(), true, "登录成功");

        // 用户登录
        StpUtil.login(sysUser.getId(), UserType.PC);

        // 缓存访问信息
        StpUtil.getTokenSession().set(SaSessionKey.ACCESS_INFO, accessInfo);

        // 获取登录信息
        LoginInfoVo loginInfo = getLoginInfo(sysUser);

        // 设置token
        loginInfo.setToken(StpUtil.getTokenValue());

        return loginInfo;
    }

    @Override
    public void logout() {
        if (StpUtil.isLogin()) {
            StpUtil.logout();
        }
    }

    @Override
    public LoginInfoVo getLoginInfo() {
        // 用户登录信息
        Long userId = LoginContext.getUserId();

        // 查询数据库
        SysUser sysUser = sysUserDao.getById(userId);

        // 获取登录信息
        return getLoginInfo(sysUser);
    }

    @Override
    public UserInfoVo getUserInfo() {
        Long userId = LoginContext.getUserId();
        SysUser sysUser = sysUserDao.getById(userId);

        UserInfoVo vo = new UserInfoVo();
        vo.setNickname(sysUser.getNickname());
        vo.setSex(sysUser.getSex());
        vo.setPhoneNumber(sysUser.getPhoneNumber());
        vo.setEmail(sysUser.getEmail());

        return vo;
    }

    @Override
    public void editUserInfo(UserInfoDto dto) {
        SysUser sysUser = new SysUser();
        sysUser.setId(LoginContext.getUserId());
        sysUser.setNickname(dto.getNickname());
        sysUser.setSex(dto.getSex());
        sysUser.setPhoneNumber(dto.getPhoneNumber());
        sysUser.setEmail(dto.getEmail());
        sysUserDao.updateById(sysUser);
    }

    @Override
    public void editPwd(EditPwdDto dto) {
        Long userId = LoginContext.getUserId();

        // 验证原密码
        SysUser systemUser = sysUserDao.getById(userId);
        boolean checkPassword = BCrypt.checkpw(dto.getOldPwd(), systemUser.getPassword());
        AssertUtil.eqTrue(checkPassword, "原密码错误");

        // 更新密码
        sysUserDao.updatePassword(userId, dto.getNewPwd());
    }

    @Override
    public Set<Long> getUserPermissionSet() {
        // 全部权限
        if (LoginContext.isAllDataSource()) {
            return null;
        }

        Set<Long> userIdSet = new HashSet<>();
        userIdSet.add(LoginContext.getUserId());

        Set<Long> deptIdSet = LoginContext.getDeptPermissionSet();
        if (ObjectUtil.isNotEmpty(deptIdSet)) {
            userIdSet.addAll(sysUserDao.getUserIdSetByDeptId(deptIdSet));
        }

        return userIdSet;
    }

    /**
     * 创建访问信息
     *
     * @return 访问信息
     */
    private AccessInfo createAccessInfo() {
        UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
        String ip = ServletUtil.getClientIP(request);
        String location = AddressUtils.getRealAddressByIP(ip);
        String browser = userAgent.getBrowser().getName();
        String os = userAgent.getOs().getName();

        AccessInfo accessInfo = new AccessInfo();
        accessInfo.setAccessTime(new Date());
        accessInfo.setIp(ip);
        accessInfo.setLocation(location);
        accessInfo.setBrowser(browser);
        accessInfo.setOs(os);

        return accessInfo;
    }

    /**
     * 账号是否被锁定
     */
    private void validateLock(LoginDto dto) {
        if (jyConfig.getMaxRetryCount() <= 0) {
            return;
        }

        // 获取登录错误次数错误
        String lockUsernameRedisKey = RedisKey.getLockUsername(dto.getUsername());
        Integer errorCount = RedisCache.get(lockUsernameRedisKey);

        // 验证账号密码是否被锁定
        if (errorCount == null || errorCount < jyConfig.getMaxRetryCount()) {
            return;
        }

        // 获取锁定时间
        Long expire = RedisCache.getExpire(lockUsernameRedisKey);
        String formatTime = DateUtil.formatBetween(expire, BetweenFormatter.Level.SECOND);

        throw new ServiceException("密码错误次数过多，账号已被锁定，请{}后再试", formatTime);
    }

    /**
     * 验证码是否正确
     */
    private void validateCaptcha(LoginDto dto) {
        String uuid = dto.getUuid();
        String code = dto.getCode();

        if (!jyConfig.getCaptchaOnOff()) {
            return;
        }

        // 验证入参
        AssertUtil.notBlank(uuid, "验证码标识不能为空");
        AssertUtil.notBlank(code, "验证码不能为空");

        // 获取验证码缓存key
        String cacheCodeKey = RedisKey.getCaptchaCode(uuid);

        // 获取验证码缓存值
        String cacheCode = RedisCache.get(cacheCodeKey);
        AssertUtil.notNull(cacheCode, "验证码已过期");

        // 删除验证码
        RedisCache.delete(cacheCodeKey);

        // 比较验证码
        AssertUtil.equals(code.toLowerCase(), cacheCode, "验证码错误");
    }

    /**
     * 用户是否正常
     *
     * @param sysUser 用户信息
     * @param dto     登录信息
     */
    private void validateUser(SysUser sysUser, LoginDto dto) {
        AssertUtil.notNull(sysUser, "用户名不存在");
        AssertUtil.equals(sysUser.getStatus(), CommonConstant.ENABLE, "账号已被禁用");

        // 验证密码
        if (BCrypt.checkpw(dto.getPassword(), sysUser.getPassword())) {
            return;
        }

        // 密码错误，增加错误次数
        incrErrorCount(dto.getUsername());
        throw new ServiceException("用户名或密码错误");
    }

    /**
     * 添加用户名锁定次数
     */
    private void incrErrorCount(String username) {
        if (jyConfig.getMaxRetryCount() > 0) {
            RedisCache.incr(RedisKey.getLockUsername(username), Duration.ofMinutes(jyConfig.getLockTime()));
        }
    }

    /**
     * 清空用户密码错误次数
     */
    private void clearUserLockCount(String username) {
        if (jyConfig.getMaxRetryCount() > 0) {
            RedisCache.delete(RedisKey.getLockUsername(username));
        }
    }

    /**
     * 记录登录日志
     */
    private void recordAccessLog(AccessInfo accessInfo, String username, boolean success, String message) {
        AccessEvent accessEvent = new AccessEvent();
        accessEvent.setAccessInfo(accessInfo);
        accessEvent.setUsername(username);
        accessEvent.setSuccess(success);
        accessEvent.setMessage(message);
        applicationContext.publishEvent(accessEvent);
    }

    /**
     * 获取登录信息
     */
    public LoginInfoVo getLoginInfo(SysUser sysUser) {
        // 获取用户启用的角色列表
        List<SysRole> sysRoleList = sysRoleService.getUserEnableList(sysUser.getId());

        // 获取用户启用菜单列表
        List<SysMenuVo> sysMenuList = sysMenuService.getUserEnableList(sysUser.getId(), sysRoleList);

        // 创建用户信息
        UserInfo userInfo = createLoginUser(sysUser, sysRoleList, sysMenuList);

        // 创建路由信息
        List<RouterVo> routers = createRouters(sysMenuList, CommonConstant.PARENT_ID);

        // 缓存用户信息
        StpUtil.getTokenSession().set(SaSessionKey.USER_INFO, userInfo);

        LoginInfoVo loginInfoVo = new LoginInfoVo();
        loginInfoVo.setSuperAdmin(CommonConstant.SUPER_ADMIN_USER_ID.equals(sysUser.getId()));
        loginInfoVo.setUserId(userInfo.getUserId());
        loginInfoVo.setUsername(userInfo.getUsername());
        loginInfoVo.setNickname(userInfo.getNickname());
        loginInfoVo.setAvatar(userInfo.getAvatar());
        loginInfoVo.setPermissions(userInfo.getApiPermissionSet());
        loginInfoVo.setRouters(routers);

        return loginInfoVo;
    }

    /**
     * 创建登录用户信息
     *
     * @param sysUser 用户信息实体
     */
    private UserInfo createLoginUser(SysUser sysUser, List<SysRole> sysRoleList, List<SysMenuVo> sysMenuList) {
        // 创建登录用户信息实体
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId(sysUser.getId());
        userInfo.setDeptId(sysUser.getDeptId());
        userInfo.setUsername(sysUser.getUsername());
        userInfo.setNickname(sysUser.getNickname());
        userInfo.setEmail(sysUser.getEmail());
        userInfo.setPhoneNumber(sysUser.getPhoneNumber());
        userInfo.setSex(sysUser.getSex());
        userInfo.setAvatar(sysUser.getAvatar());

        if (CommonConstant.SUPER_ADMIN_USER_ID.equals(sysUser.getId())) {
            userInfo.setRoleIdSet(Collections.emptySet());
            userInfo.setRoleCodeSet(Collections.emptySet());
            userInfo.setDataPermissionSet(Collections.emptySet());
            userInfo.setDeptPermissionSet(Collections.emptySet());
            userInfo.setApiPermissionSet(Collections.emptySet());
        } else {
            Set<Long> roleIdSet = sysRoleList.stream().map(BaseIdPo::getId).collect(Collectors.toSet());
            Set<String> roleCodeSet = sysRoleList.stream().map(SysRole::getCode).collect(Collectors.toSet());
            Set<Integer> dataPermissionSet = sysRoleList.stream().map(SysRole::getDataScope).collect(Collectors.toSet());
            Set<Long> deptPermissionSet = getDeptPermissionSet(sysUser.getDeptId(), roleIdSet, dataPermissionSet);
            Set<String> apiPermissionSet = sysMenuList.stream().map(SysMenu::getPerms)
                    .flatMap(item -> Arrays.stream(item.split(StringPool.COMMA)))
                    .filter(StrUtil::isNotBlank).collect(Collectors.toSet());

            userInfo.setRoleIdSet(roleIdSet);
            userInfo.setRoleCodeSet(roleCodeSet);
            userInfo.setDataPermissionSet(dataPermissionSet);
            userInfo.setDeptPermissionSet(deptPermissionSet);
            userInfo.setApiPermissionSet(apiPermissionSet);
        }

        return userInfo;
    }

    /**
     * 把菜单封装成前端所需格式
     *
     * @param menus    菜单列表
     * @param parentId 父级菜单id
     * @return 前端所需格式
     */
    private List<RouterVo> createRouters(List<SysMenuVo> menus, Long parentId) {
        List<RouterVo> routerVoList = new ArrayList<>();

        menus.forEach(sysMenu -> {

            if (ObjectUtil.notEqual(sysMenu.getParentId(), parentId)) {
                return;
            }

            if (MenuConstant.BUTTON.equals(sysMenu.getType())) {
                return;
            }

            RouterVo routerVo = new RouterVo();
            routerVo.setName(sysMenu.getName());
            routerVo.setPath(sysMenu.getPath());
            routerVo.setComponent(routerVo.getPath() + "/index");
            routerVo.setMeta(getMetaVo(sysMenu));

            // 如果是目录，递归封装菜单
            if (MenuConstant.DIRECTORY.equals(sysMenu.getType())) {
                // 菜单列表
                List<RouterVo> routerVoItemList = createRouters(menus, sysMenu.getId());
                // 判断菜单是否为空，空则不显示目录
                if (routerVoItemList.isEmpty()) {
                    return;
                }
                routerVo.setChildren(routerVoItemList);
            }

            routerVoList.add(routerVo);
        });

        return routerVoList;
    }

    /**
     * 获取目录其他元素
     *
     * @param sysMenu 菜单信息
     * @return 目录其他元素
     */
    private MetaVo getMetaVo(SysMenu sysMenu) {
        MetaVo metaVo = new MetaVo();
        metaVo.setTitle(sysMenu.getTitle());
        metaVo.setIcon(sysMenu.getIcon());
        metaVo.setActiveMenu(sysMenu.getActiveMenu());
        metaVo.setIsHide(CommonConstant.YES.equals(sysMenu.getIsHide()));
        metaVo.setIsKeepAlive(CommonConstant.YES.equals(sysMenu.getIsKeepAlive()));
        metaVo.setIsLink(CommonConstant.YES.equals(sysMenu.getIsLink()));
        metaVo.setIsFull(CommonConstant.YES.equals(sysMenu.getIsFull()));
        return metaVo;
    }

    /**
     * 获取可访问数据部门Id集合
     *
     * @return 可访问数据部门Id集合
     */
    private Set<Long> getDeptPermissionSet(Long deptId, Set<Long> roleIdSet, Set<Integer> dataPermissionSet) {
        Set<Long> deptIdSet = new HashSet<>();

        // 本部门以及下级部门权限
        if (dataPermissionSet.contains(DataScopeConstant.DEPARTMENT_AND_BELOW)) {
            List<Long> childrenIdList = sysDeptDao.getChildrenIdList(deptId);
            deptIdSet.add(deptId);
            deptIdSet.addAll(childrenIdList);
        }

        // 本部门权限
        else if (dataPermissionSet.contains(DataScopeConstant.DEPARTMENT)) {
            deptIdSet.add(deptId);
        }

        // 自定义权限
        if (dataPermissionSet.contains(DataScopeConstant.CUSTOM)) {
            List<Long> deptIdList = sysRoleDeptDao.getDeptIdListByRoleId(roleIdSet);
            deptIdSet.addAll(deptIdList);
        }

        return deptIdSet;
    }

}
