package com.backendsys.modules.system.service.impl;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.backendsys.exception.CustException;
import com.backendsys.modules.common.config.redis.utils.RedisUtil;
import com.backendsys.modules.common.config.security.entity.SecurityUserInfo;
import com.backendsys.modules.common.config.security.utils.*;
import com.backendsys.modules.system.dao.SysMobileAreaDao;
import com.backendsys.modules.system.dao.SysUserDao;
import com.backendsys.modules.system.dao.SysUserInfoDao;
import com.backendsys.modules.system.dao.SysUserRoleDao;
import com.backendsys.modules.system.entity.*;
import com.backendsys.modules.system.service.SysAuthService;
import com.backendsys.modules.system.service.SysCommonService;
import com.backendsys.modules.system.service.SysUserIntegralService;
import com.backendsys.modules.system.service.SysUserService;
import com.backendsys.utils.response.ResultEnum;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.code.kaptcha.Producer;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Service
public class SysAuthServiceImpl implements SysAuthService {

    @Autowired
    private Environment env;
    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private TokenUtil tokenUtil;
    @Autowired
    private HttpRequestUtil httpRequestUtil;
    @Autowired
    private LockStatusUtil lockStatusUtil;
    @Autowired
    private CaptchaUtil captchaUtil;
    @Autowired
    private Producer captchaProducer;

    @Autowired
    private SysUserDao sysUserDao;
    @Autowired
    private SysUserRoleDao sysUserRoleDao;
    @Autowired
    private SysUserInfoDao sysUserInfoDao;
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysMobileAreaDao sysMobileAreaDao;
    @Autowired
    private SysUserIntegralService sysUserIntegralService;
    @Autowired
    private SysCommonService sysCommonService;

    @Value("${tencent.sms.debug}")
    private String SMS_DEBUG;
    @Value("${CAPTCHA_DURATION}")
    private Integer CAPTCHA_DURATION;
    @Value("${REDIS_LOGIN_TOKEN_PREFIX}")
    private String REDIS_LOGIN_TOKEN_PREFIX;
    @Value("${spring.application.name}")
    private String APPLICATION_NAME;

    @Override
    public void renderCaptcha(HttpServletResponse response) throws IOException {
        byte[] captchaChallengeAsJpeg;
        ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
        try {
            String createText = captchaProducer.createText();
            // 获得当前 (UA + IP) 生成的 Key
            String captchaRedisKey = httpRequestUtil.getKaptchaKey();
            // 保存 验证码字符串 到 redis 中
            redisUtil.setCacheObject(captchaRedisKey, createText, this.CAPTCHA_DURATION, TimeUnit.MILLISECONDS);
            // 返回 BufferedImage 对象并转为 byte 写入到 byte 数组中
            BufferedImage challenge = captchaProducer.createImage(createText);
            ImageIO.write(challenge, "jpg", jpegOutputStream);
        } catch (Exception e) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
        // 定义response输出类型为image/jpeg类型，使用response输出流输出图片的byte数组
        captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
        response.setHeader("Cache-Control", "no-store");
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", 0);
        response.setContentType("image/jpeg");

        ServletOutputStream responseOutputStream = response.getOutputStream();
        responseOutputStream.write(captchaChallengeAsJpeg);
        responseOutputStream.flush();
        responseOutputStream.close();
    }

    // 判断是否需验证码登录状态
    @Override
    public Map<String, Object> checkCaptchaRequired(String username) {
        if (StrUtil.isEmpty(username)) throw new CustException("username 不能为空");
        Boolean currentCaptchaRequired =  captchaUtil.isCaptchaRequired(APPLICATION_NAME + "-login-required-captcha-" + username, 3);
        return Map.of("is_captcha_required", currentCaptchaRequired);
    }

    @Override
    @Cacheable(value = "catch::mobile-area", key = "'list'", unless = "#result == null")
    public List<SysMobileArea> getMobileAreaList(SysMobileArea sysMobileArea) {
        return sysMobileAreaDao.selectMobileAreaList(sysMobileArea);
    }

    // [方法] 登录失败 (通用) (errMsg: 错误提示文本, username: 用户名, intercept: 是否拦截)
    public void loginFail(String errMsg, String username, Boolean isIntercept) {

        // 验证码是否必填
        Boolean currentCaptchaRequired = captchaUtil.isCaptchaRequired(APPLICATION_NAME + "-login-required-captcha-" + username, 3);
        System.out.println("(loginFailByUsername) currentCaptchaRequired = " + currentCaptchaRequired);

        // 删除图形验证码
        redisUtil.delete(httpRequestUtil.getKaptchaKey());
        // 添加登录错误的冻结标记
        if (isIntercept) lockStatusUtil.setLockStatus(APPLICATION_NAME + "-login-error", username);

        if (currentCaptchaRequired) {
            throw new CustException(errMsg, ResultEnum.INVALID_CREDENTIALS.getCode(), Map.of("is_captcha_required", true));
        } else {
            throw new CustException(errMsg, ResultEnum.INVALID_CREDENTIALS.getCode());
        }

    }

    // [方法] 登录成功
    public SysUserInfo loginSuccess(Long user_id, Integer is_remember) {

        // [查询] 登录的用户信息
        SysUserInfo sysUserInfo = sysUserService.selectUserInfo(user_id);

        // 删除图形验证码缓存
        redisUtil.delete(httpRequestUtil.getKaptchaKey());

        // 删除旧的登录缓存
        tokenUtil.deleteRedisLoginToken(sysUserInfo.getLast_login_uuid());

        // 判断用户是否审核
        Integer audit_status = sysUserInfo.getAudit_status();
        if (audit_status != null && audit_status.equals(1)) throw new CustException("用户正在审核中");
        if (audit_status != null && audit_status.equals(-1)) throw new CustException("用户审核未通过，请与客服联系");

        // 判断用户是否启用
        Integer status = sysUserInfo.getStatus();
        if (status != null && status.equals(-1)) throw new CustException("该用户已被禁用，请与客服联系");

        // 判断用户是否已删除
        Integer del_flag = sysUserInfo.getDel_flag();
        if (del_flag != null && del_flag.equals(1)) throw new CustException("当前用户不可用，请与客服联系");

        // 设置 最后一次的登录信息 (uuid, ip, 登录时间)
        String uuid = Convert.toStr(UUID.randomUUID());
        sysUserInfo.setLast_login_uuid(uuid);
        sysUserInfo.setLast_login_ip(httpRequestUtil.getIpAddr());
//        sysUserInfo.setLast_login_time(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
        sysUserInfo.setLast_login_time(LocalDateTime.now(ZoneOffset.UTC));
        sysUserInfoDao.updateById(sysUserInfo);

        // [系统配置] 系统用户默认登录过期时间(小时)
        Integer SYSTEM_USER_LOGIN_DURATION_DEFAULT = Convert.toInt(sysCommonService.getCommonByTag("SYSTEM_USER_LOGIN_DURATION_DEFAULT"));
        // 将小时转换为毫秒
        Long DEFAULT_MILLISECONDS = SYSTEM_USER_LOGIN_DURATION_DEFAULT * DateUnit.HOUR.getMillis();
        // 7天 (转毫秒)
        Long SEVEN_DAY_MILLISECONDS = 7L * 24 * 60 * 60 * 1000;

        Long token_duration_milliseconds = (is_remember != null && is_remember.equals(1)) ? SEVEN_DAY_MILLISECONDS : DEFAULT_MILLISECONDS;
        Integer token_duration_hours = Convert.toInt(token_duration_milliseconds / 3600000L);

        Date token_expiration = new Date((new Date()).getTime() + token_duration_milliseconds);
        sysUserInfo.setToken_expiration(DateUtil.format(token_expiration, "yyyy-MM-dd HH:mm:ss"));

        // 生成 Token
        SecurityUserInfo securityUserInfo = JSONUtil.toBean(JSONUtil.parseObj(sysUserInfo), SecurityUserInfo.class);
        String token = jwtUtil.createSystemJwtToken(securityUserInfo);
        String token_redis_key = REDIS_LOGIN_TOKEN_PREFIX + uuid;
        sysUserInfo.setToken(token);

        // 生成 PerMissionIds
        List<String> permission_ids_list = sysUserInfo.getPermission_ids();

        // [Redis] 将 Token 与 Permission 存入缓存
        TokenCatch tokenCatch = new TokenCatch(token, permission_ids_list);
        redisUtil.setCacheObject(token_redis_key, JSONUtil.toJsonStr(tokenCatch), token_duration_hours, TimeUnit.HOURS);

        return sysUserInfo;
    }

    private void setLoginRequired(String key) {
        Object captchaValue = redisUtil.getCacheObject(APPLICATION_NAME + "-login-required-captcha-" + key);
        Integer currentErrCount = (captchaValue == null) ? 1 : (Convert.toInt(captchaValue) + 1);
        redisUtil.setCacheObject(APPLICATION_NAME + "-login-required-captcha-" + key, currentErrCount, 1, TimeUnit.MINUTES);
        System.out.println("currentErrCount: " + currentErrCount);
    }
    public void cleanLoginRequired(String key) {
        redisUtil.delete(APPLICATION_NAME + "-login-required-captcha-" + key);
    }

    /**
     * 登录 (用户名)
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public SysUserInfo login(SysAuth sysAuth) {

        String username = sysAuth.getUsername();
        String password = sysAuth.getPassword();
        String captcha = sysAuth.getCaptcha();

        // 判断是否处于登录错误的冻结状态 (2分钟内错误5次，则出现冻结提示)
        lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-login-error", username);

        // -- 判断是否需要输入验证码 ----------------------------------------------------
        // - 当输错 3 次密码时，需要输入验证码
        // - 当输错后 1 分钟后重置
        Boolean isCaptchaRequired = captchaUtil.isCaptchaRequired(APPLICATION_NAME + "-login-required-captcha-" + username, 3);
        if (isCaptchaRequired) {
            Boolean isCaptchaEmpty = StrUtil.isEmpty(captcha);
            Boolean isCpatchaValid = (captchaUtil.isCaptchaValid(captcha, httpRequestUtil.getKaptchaKey()));
            if (isCaptchaEmpty) { loginFail("验证码不能为空", username, false); return null; }
            if (!isCpatchaValid) { loginFail("验证码错误", username, false); return null; }
        }
        // --------------------------------------------------------------------------

        // [Method] 判断 用户 是否存在 && 密码是否正确
        SysUser sysUser = sysUserDao.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
        if (sysUser == null) {
            // 输入错误时，计数器叠加，并且设置重置时间 (会一直叠加，直到重置 或 登录成功)
            setLoginRequired(username);
            // [登录失败] 用户不存在
            loginFail("用户名或密码错误", username, true);
            return null;
        } else {
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            if (!encoder.matches(password, sysUser.getPassword())) {
                // 输入错误时，计数器叠加，并且设置重置时间 (会一直叠加，直到重置 或 登录成功)
                setLoginRequired(username);
                // [登录失败] 密码不正确
                loginFail("用户名或密码错误", username, true);
            }
            // [登录成功]
            cleanLoginRequired(username);
            return loginSuccess(sysUser.getId(), sysAuth.getIs_remember());
        }

    }

    /**
     * 登录 (手机号码)
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public SysUserInfo loginWithPhone(SysAuthPhone sysAuthPhone) {

        String phone = sysAuthPhone.getPhone();
        Integer phoneAreaCode = sysAuthPhone.getPhone_area_code();
        Integer phoneValidCode = sysAuthPhone.getPhone_valid_code();

        // 判断是否处于登录错误的冻结状态 (2分钟内错误5次，则出现冻结提示)
        lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-login-error", phone);

        // 判断短信验证码是否正确
        String redisKey = APPLICATION_NAME + "-sms-login" + "-" + phone;
        Integer smsCode = redisUtil.getCacheObject(redisKey);
        // 判断是否发送验证码
        if ("false".equals(SMS_DEBUG) && smsCode == null) throw new CustException("请先发送短信验证码");
        // 判断短信验证码是否错误
        if ("false".equals(SMS_DEBUG) && !smsCode.equals(phoneValidCode)) loginFail("短信验证码错误", phone, true);

        // 判断手机号是否存在
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysUser::getPhone, phone).eq(SysUser::getPhone_area_code, phoneAreaCode);
        SysUser sysUser = sysUserDao.selectOne(queryWrapper);
        if (sysUser == null) {
            // [登录失败] 用户不存在 (并不会销毁短信验证码)
            loginFail("手机号码未注册，请先注册", phone, true);
            return null;
        } else {
            // 登录成功，销毁短信验证码
            redisUtil.delete(redisKey);
            // [登录成功]
            return loginSuccess(sysUser.getId(), sysAuthPhone.getIs_remember());
        }

    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> register(SysUserDTO sysUserDTO) {

        // 判断是否允许注册
        // [系统配置] 是否允许系统用户注册
        Boolean SYSTEM_USER_ALLOW_REGISTER = Convert.toBool(sysCommonService.getCommonByTag("SYSTEM_USER_ALLOW_REGISTER"));
        if (!SYSTEM_USER_ALLOW_REGISTER) throw new CustException("系统已禁止注册");

        // -- 参数校验 --------------------------------------------------------------
        String username = sysUserDTO.getUsername();
        String password = sysUserDTO.getPassword();
        String captcha = sysUserDTO.getCaptcha();

        String phone = sysUserDTO.getPhone();
        Integer phoneAreaCode = sysUserDTO.getPhone_area_code();
        Integer phoneValidCode = sysUserDTO.getPhone_valid_code();

        String activeProfile = env.getActiveProfiles()[0];
        if (!"local".equals(activeProfile)) {
            // 判断是否处于登录错误的冻结状态 (2分钟内错误5次，则出现冻结提示)
            lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-register-error", username);
            lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-register-error", phone);
            // 判断图形验证码是否正确
            if (!captchaUtil.isCaptchaValid(captcha, httpRequestUtil.getKaptchaKey())) {
                loginFail("验证码错误", username, false);
                return null;
            }
        }

        // [查询] 判断用户名是否存在
        SysUser sysUser1 = sysUserDao.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
        if (sysUser1 != null) throw new CustException("用户名 (" + username + ") 已被注册");
        
        // 判断短信验证码是否正确
        if (!"local".equals(activeProfile)) {
            String redisKey = APPLICATION_NAME + "-sms-register" + "-" + phone;
            Integer smsCode = redisUtil.getCacheObject(redisKey);
            // 判断是否发送验证码
            if ("false".equals(SMS_DEBUG) && smsCode == null) throw new CustException("请先发送短信验证码");
            // 判断短信验证码是否错误
            if ("false".equals(SMS_DEBUG) && !smsCode.equals(phoneValidCode)) loginFail("短信验证码错误", phone, true);
        }


        // [查询] 判断手机号是否存在
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysUser::getPhone, phone).eq(SysUser::getPhone_area_code, phoneAreaCode);
        SysUser sysUser2 = sysUserDao.selectOne(queryWrapper);
        if (sysUser2 != null) throw new CustException("手机号码 (+" + phoneAreaCode + " " + phone + ") 已被注册");
        
        // -- 通过校验 --------------------------------------------------------------

        // 密码二次加密
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String encodedPassword = encoder.encode(password);
        sysUserDTO.setPassword(encodedPassword);

        // 注册
        SysUserDTO registerEntity = new SysUserDTO();
        registerEntity.setUsername(sysUserDTO.getUsername());
        registerEntity.setPhone(sysUserDTO.getPhone());
        registerEntity.setPhone_area_code(sysUserDTO.getPhone_area_code());
        registerEntity.setPassword(sysUserDTO.getPassword());


        // 做成后台可控制？


        // 邀请码
        registerEntity.setInvite_code(sysUserDTO.getInvite_code());

        // 注册时，默认使用 权限 (DEFAULT)
        String role_sign = "DEFAULT";

        // 如果邀请码是 (Material)，则注册成为 [素材游客]
        if ("Material".equals(sysUserDTO.getInvite_code())) role_sign = "MATERIAL_GUEST";

        LambdaQueryWrapper<SysUserRole> wrapperRole = new LambdaQueryWrapper<>();
        wrapperRole.eq(SysUserRole::getRole_sign, role_sign);
        SysUserRole roleDetail = sysUserRoleDao.selectOne(wrapperRole);
        registerEntity.setRole_id(Arrays.asList(roleDetail.getRole_id()));

        // 注册时，审核状态为 待审核 (-1拒绝, 1待审核, 2审核通过)
        registerEntity.setAudit_status(1);

        // 注册时，状态为 禁用
        // registerEntity.setStatus(-1);

        // 随机昵称 (6位)
        registerEntity.setNickname("用户" + RandomUtil.randomStringUpper(6));

        // 创建用户
        sysUserDao.insertUser(registerEntity);

        // 初始化用户积分
        sysUserIntegralService.init(registerEntity.getId());

        return Map.of("user_id", registerEntity.getId());
    }

    /**
     * 忘记密码/重置密码
     */
    @Override
    public Map<String, Object> forgotPassword(SysUserDTO sysUserDTO) {

        String phone = sysUserDTO.getPhone();
        Integer phoneAreaCode = sysUserDTO.getPhone_area_code();
        Integer phoneValidCode = sysUserDTO.getPhone_valid_code();

        // 判断短信验证码是否正确
        String redisKey = APPLICATION_NAME + "-sms-forgotPassword-" + sysUserDTO.getPhone();
        Integer smsCode = redisUtil.getCacheObject(redisKey);
        if ("false".equals(SMS_DEBUG) && (smsCode == null || !smsCode.equals(phoneValidCode))) {
            throw new CustException("短信验证码错误");
        }

        // [查询] 判断手机号是否存在
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysUser::getPhone, phone).eq(SysUser::getPhone_area_code, phoneAreaCode);
        SysUser sysUser = sysUserDao.selectOne(queryWrapper);
        if (sysUser == null) throw new CustException("手机号码不存在");

        // 密码二次加密
        String password = sysUserDTO.getPassword();
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String encodedPassword = encoder.encode(password);
        sysUser.setPassword(encodedPassword);

        // 编辑密码
        sysUserDao.updateById(sysUser);

        // 更改成功，销毁短信验证码
        redisUtil.delete(redisKey);

        return Map.of("user_id", sysUser.getId());
    }

    /**
     * 退出登录
     */
    public Map<String, Object> logout() {
        Long user_id = httpRequestUtil.getUserId();
        if (user_id != null) {
            tokenUtil.deleteRedisLoginToken(null);
        }
        return Map.of("user_id", user_id);
    }





}
