package com.cf.imes.module.system.service.sms;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.cf.imes.framework.security.core.LoginUser;
import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginSmsCheckReqVO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.dal.redis.RedisKeyConstants;
import com.cf.imes.module.system.enums.sms.SmsSceneEnum;
import com.cf.imes.module.system.framework.sms.config.SmsCodeProperties;
import com.cf.imes.module.system.enums.ErrorCodeConstants;
import com.cf.imes.module.system.service.user.AdminUserService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import jakarta.annotation.Resource;
import java.time.Duration;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

import static cn.hutool.core.util.RandomUtil.randomInt;

/**
 * 短信验证码 Service 实现类
 *
 * @author 晨丰科技
 */
@Service
@Validated
public class SmsCodeServiceImpl implements SmsCodeService {

    @Resource
    private SmsCodeProperties smsCodeProperties;

    @Resource
    private SmsSendService smsSendService;

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private AdminUserService userService;

    @Override
    public void sendSmsCode(SmsCodeSendReqDTO reqDTO) {
        AdminUserDO user = userService.getUserByUsernameAndOrganId(reqDTO.getMobile(), null);
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        // 更新手机号场景检查
        if (ObjectUtil.equal(SmsSceneEnum.USER_UPDATE_MOBILE.getScene(), reqDTO.getScene())) {
            userUpdateMobileValid(user, loginUser);
        }
        // 非修改手机号的场景时校验手机号是否存在
        if (ObjectUtil.isNull(user) && ObjectUtil.notEqual(SmsSceneEnum.USER_UPDATE_MOBILE.getScene(), reqDTO.getScene())) {
            throw new ServiceException(ErrorCodeConstants.AUTH_MOBILE_NOT_EXISTS);
        }
        SmsSceneEnum sceneEnum = SmsSceneEnum.getCodeByScene(reqDTO.getScene());
        Assert.notNull(sceneEnum, "验证码场景({}) 查找不到配置", reqDTO.getScene());
        // 创建验证码
        String code = createSmsCode(reqDTO.getMobile(), reqDTO.getCreateIp());
        // 发送验证码
        HashMap<String, Object> map = MapUtil.of("code", code);
        map.put("product","晨丰科技");
        smsSendService.sendSingleSms(reqDTO.getMobile(), null, null,
                sceneEnum.getTemplateCode(), map);
    }

    /**
     * 更新手机号的场景检查
     *
     * @param user
     * @param loginUser
     */
    private void userUpdateMobileValid(AdminUserDO user, LoginUser loginUser) {
        if (ObjectUtil.isNull(loginUser)) {
            throw new ServiceException(GlobalErrorCodeConstants.UNAUTHORIZED);
        }
        if (ObjectUtil.isNotNull(user)) {
            if (ObjectUtil.equal(user.getId(), loginUser.getId())) {
                // 手机号没有改变无需验证码
                throw new ServiceException(ErrorCodeConstants.AUTH_MOBILE_NO_CHANGE);
            } else {
                // 修改手机号的场景校验手机是否已存在
                throw new ServiceException(ErrorCodeConstants.USER_MOBILE_EXISTS);
            }
        }
    }

    /**
     * 生成验证码
     *
     * @param mobile 手机号
     * @param ip     请求来源ip
     * @return
     */
    private String createSmsCode(String mobile, String ip) {
        String redisMobileKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION, mobile);
        String redisMobileCountKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION_LIMIT, mobile);
        String redisIpCountKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION_LIMIT, ip);
        boolean hasCode = redisTemplate.hasKey(redisMobileKey);
        if (!hasCode) {
            // 验证码不存在
            Duration sendWindowSizeMin = smsCodeProperties.getSendWindowSizeMin();
            Integer sendMaxSendTimesPerWindow = smsCodeProperties.getSendMaxSendTimesPerWindow();
            // 限制手机号请求次数：10分钟5次
            boolean mobileCountRateLimit = slidingWindowRateLimit(redisMobileCountKey, sendWindowSizeMin.toMinutesPart(), sendMaxSendTimesPerWindow);
            if (!mobileCountRateLimit) {
                throw new ServiceException(ErrorCodeConstants.SMS_CODE_SEND_TOO_FAST);
            }
            // 限制ip请求次数
            boolean ipCountRateLimit = slidingWindowRateLimit(redisIpCountKey, sendWindowSizeMin.toMinutesPart(), sendMaxSendTimesPerWindow);
            if (!ipCountRateLimit) {
                throw new ServiceException(ErrorCodeConstants.SMS_CODE_SEND_TOO_FAST);
            }
        } else {
            // 验证码存在提示请勿重复发送
            throw new ServiceException(ErrorCodeConstants.SMS_CODE_IS_VALID);
        }

        // 生成6位随机验证码
        StringBuffer code = new StringBuffer(6);
        for (int i = 0; i < 6; i++) {
            code.append(randomInt(0, 9));
        }
        return code.toString();
    }

    /**
     * 滑动窗口限制发送次数
     *
     * @param key
     * @param winSizeMin         窗口时间大小
     * @param maxSendTimesPerWin 窗口内最大请求次数
     * @return false没有达到发送频繁告警要求
     */
    public boolean slidingWindowRateLimit(String key, int winSizeMin, int maxSendTimesPerWin) {
        // 获取当前时间戳
        long currentTime = System.currentTimeMillis();

        // 清除过期的时间片
        redisTemplate.opsForZSet().removeRangeByScore(key, 0L, currentTime - (winSizeMin * 60 * 1000));

        // 检查过去 winSizeMin 分钟内的请求次数
        long requestCount = redisTemplate.opsForZSet().zCard(key);
        if (requestCount >= maxSendTimesPerWin) {
            return false;
        }

        // 窗口足够添加当前请求的时间戳
        redisTemplate.opsForZSet().add(key, currentTime, currentTime);

        // 设置过期时间 = 窗口大小 + 30分钟缓冲
        redisTemplate.expire(key, winSizeMin + 30, TimeUnit.MINUTES);
        return true;
    }

    @Override
    public void useSmsCode(SmsCodeUseReqDTO reqDTO) {
        // 非修改手机号的场景时校验手机号是否存在
        AdminUserDO user = userService.getUserUniqueByUserName(reqDTO.getMobile());
        if (ObjectUtil.isNull(user) && ObjectUtil.notEqual(SmsSceneEnum.USER_UPDATE_MOBILE.getScene(), reqDTO.getScene())) {
            throw new ServiceException(ErrorCodeConstants.AUTH_MOBILE_NOT_EXISTS);
        }
        String smsCodeKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION, reqDTO.getMobile());
        Object keyObj = redisTemplate.opsForValue().get(smsCodeKey);
        if (ObjectUtil.isNotNull(keyObj)) {
            // 校验码存在
            if (ObjectUtil.notEqual(String.valueOf(keyObj), reqDTO.getCode())) {
                // 验证码不正确
                throw new ServiceException(ErrorCodeConstants.SMS_CODE_NOT_CORRECT);
            } else {
                // 消耗验证码
                redisTemplate.delete(smsCodeKey);
            }
        } else {
            // 提示验证码不存在
            throw new ServiceException(ErrorCodeConstants.SMS_CODE_NOT_FOUND);
        }
    }

    @Override
    public void validateSmsCode(AuthLoginSmsCheckReqVO reqVO) {
        String mobile = reqVO.getMobile();
        // 非修改手机号的场景时校验手机号是否存在
        AdminUserDO user = userService.getUserUniqueByUserName(mobile);
        if (ObjectUtil.isNull(user)) {
            throw new ServiceException(ErrorCodeConstants.AUTH_MOBILE_NOT_EXISTS);
        }
        String smsCodeKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION, mobile);
        Object keyObj = redisTemplate.opsForValue().get(smsCodeKey);
        if (ObjectUtil.isNotNull(keyObj)) {
            // 校验码存在
            if (ObjectUtil.notEqual(String.valueOf(keyObj), reqVO.getSmsCaptchaVerification())) {
                // 验证码不正确
                throw new ServiceException(ErrorCodeConstants.SMS_CODE_NOT_CORRECT);
            }
        } else {
            // 提示验证码不存在
            throw new ServiceException(ErrorCodeConstants.SMS_CODE_NOT_FOUND);
        }
    }

}
