package cn.quantgroup.xyqb.aspect.captcha;


import cn.quantgroup.xyqb.Constants;
import cn.quantgroup.xyqb.model.JsonResult;
import cn.quantgroup.xyqb.thirdparty.jcaptcha.AbstractManageableImageCaptchaService;
import cn.quantgroup.xyqb.util.IpUtil;
import cn.quantgroup.xyqb.util.ValidationUtil;
import com.octo.captcha.service.CaptchaServiceException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 限次图形验证码校验标记
 *
 * @author 任文超
 * @version 1.0.0
 * @since 2017-11-07
 */
@Slf4j
@Aspect
@Component
public class CaptchaFiniteValidateAdvisor {

    private static final String SUPER_CAPTCHA_ID = UUID.nameUUIDFromBytes("__QG_APPCLIENT_AGENT__".getBytes(Charset.forName("UTF-8"))).toString();
    private static final String SUPER_CAPTCHA = "__SUPERQG__";

    private static final String ALERT_TEMP = "密码错误已超过次数，请%s分钟后再试";
    /**
     * 时间锁定反的code
     */
    private static final Long TOME_LIMIT_CODE = 3L;
    @Autowired
    @Qualifier("stringRedisTemplate")
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    @Qualifier("customCaptchaService")
    private AbstractManageableImageCaptchaService imageCaptchaService;

    /**
     * 自动化测试忽略验证码
     */
    @Value("${xyqb.auth.captcha.autotest.enable:false}")
    private boolean autoTestCaptchaEnabled;

    /**
     * 限次图形验证码切面
     */
    @Pointcut("@annotation(cn.quantgroup.xyqb.aspect.captcha.CaptchaFiniteValidator)")
    private void needCaptchaFiniteValidate() {
    }

    /**
     * 在受保护的接口方法执行前, 执行限次图形验证码校验
     *
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("needCaptchaFiniteValidate()")
    private Object doCapchaValidate(ProceedingJoinPoint pjp) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Map<String, String> phonePasswordMap = getHeaderParam(request);
        if (phonePasswordMap == null || phonePasswordMap.isEmpty()) {
            return JsonResult.buildErrorStateResult("用户名或密码不正确", null);
        }
        // 当前用户手机号
        String phoneNo = phonePasswordMap.get(Constants.PHONE_NO);
        Long countErrorByPhone = getCount(phoneNo);
        if (countErrorByPhone == null) {
            log.info("用户名或密码不正确, phoneNo={}, countErrorByPhone={}, clientIp={}", phoneNo, countErrorByPhone, IpUtil.getRemoteIP(request));
            return JsonResult.buildErrorStateResult("用户名或密码不正确", null);
        }
        if (countErrorByPhone > Constants.Image_Need_Count) {
            /**
             * 输入密码错误超过一定次数限制登陆
             */
            if (countErrorByPhone >= Constants.PASSWORD_ERROR_LOCK_COUNT) {

                String lock_key = Constants.PASSWORD_LOCK_PRE.concat(phoneNo);

                Long expire = redisTemplate.opsForValue().getOperations().getExpire(lock_key);
                /**
                 * 查看锁的剩余时间不存在说明已经解锁
                 */
                if (null != expire && expire > 0L) {
                    /**
                     * 获取到的时间是秒 转化为分 大概转一下吧不要太精确
                     */
                    expire = expire > 60L ? expire / 60 + 1 : 1L;
                    return JsonResult.buildErrorStateResult(String.format(ALERT_TEMP, expire), null, TOME_LIMIT_CODE);
                }
            }

            String registerFrom = Optional.ofNullable(request.getParameter("registerFrom")).orElse("");
            String captchaId = Optional.ofNullable(request.getParameter(Constants.QG_CAPTCHA_ID)).orElse("");
            String captchaValue = request.getParameter(Constants.QG_CAPTCHA_VALUE);
            if (shouldSkipCaptchaValidate(registerFrom, captchaId, captchaValue)) {
                log.info("使用超级图形验证码校验, registerFrom={}, clientIp={}", registerFrom, IpUtil.getRemoteIP(request));
                return pjp.proceed();
            }
            if (StringUtils.isNotBlank(captchaValue)) {
                // 忽略用户输入的大小写
                String captcha = StringUtils.lowerCase(captchaValue);
                String val = redisTemplate.opsForValue().get(Constants.IMAGE_CAPTCHA_REDIS_CACHE_KEY.concat(captchaId));

                if(null == val){
                    return JsonResult.buildSuccessResult("图形验证码已过期", "", 2L);
                }
                // 验证码校验
                Boolean validCaptcha = false;
                try {
                    validCaptcha = imageCaptchaService.validateResponseForID(Constants.IMAGE_CAPTCHA_KEY + captchaId, captcha);
                } catch (CaptchaServiceException ex) {
                    log.error("验证码校验异常, {}, {}", ex.getMessage(), ex);
                }
                if (validCaptcha) {
                    return pjp.proceed();
                }
                return JsonResult.buildSuccessResult("验证码不正确", "", 2L);
            }
            return JsonResult.buildSuccessResult("请输入图形验证码", "", 2L);
        }
        return pjp.proceed();
    }

    private boolean shouldSkipCaptchaValidate(String registerFrom, String captchaId, Object captchaValue) {
        // 如果启用了超级验证码功能, 检查超级验证码, 超级验证码区分大小写
        if (autoTestCaptchaEnabled) {
            return true;
        }
        return StringUtils.equals(SUPER_CAPTCHA_ID, String.valueOf(captchaId)) && StringUtils.equals(SUPER_CAPTCHA, String.valueOf(captchaValue));
    }

    // 获取该账号密码错误计数器
    private Long getCount(String phoneNo) {
        String key = getKey(phoneNo);
        if (StringUtils.isBlank(key)) {
            return null;
        }
        String countString = redisTemplate.opsForValue().get(key);
        if (StringUtils.isBlank(countString)) {
            return 0L;
        }
        return Long.valueOf(countString);
    }

    private String getKey(String phoneNo) {
        if (StringUtils.isBlank(phoneNo)) {
            return null;
        }
        return Constants.REDIS_PASSWORD_ERROR_COUNT + phoneNo;
    }

    /**
     * 账密参数解析
     *
     * @param request 当前请求
     * @return 账密参数Map 或 null
     */
    private Map<String, String> getHeaderParam(HttpServletRequest request) {
        String credential = request.getHeader("authorization");
        if (StringUtils.isBlank(credential) || !credential.startsWith(Constants.PASSWORD_HEADER)) {
            log.info("参数无效, credential:{}", credential);
            return null;
        }
        credential = credential.substring(Constants.PASSWORD_HEADER.length());
        byte[] buf = Base64.decodeBase64(credential);
        credential = new String(buf, Charset.forName("UTF-8"));
        String[] credentialArr = credential.split(":");
        boolean headerParamValid = credentialArr.length == 2;
        if (!headerParamValid) {
            log.info("参数无效, credential:{}", credential);
            return null;
        }
        // 当前用户手机号和密码
        String phoneNo = credentialArr[0];
        String password = credentialArr[1];
        headerParamValid = headerParamValid && ValidationUtil.validatePhoneNo(phoneNo) && StringUtils.isNotBlank(password);
        if (!headerParamValid) {
            log.info("参数无效, credential:{}, phoneNo:{}, password:{}", credential, phoneNo, password);
            return null;
        }
        log.info("账密登录, phoneNo:{}", phoneNo);
        Map<String, String> phonePasswordMap = new HashMap<>(2);
        phonePasswordMap.put(Constants.PHONE_NO, phoneNo);
        phonePasswordMap.put("password", password);
        return phonePasswordMap;
    }


}
