package com.qunzhixinxi.hnqz.admin.aspect;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.qunzhixinxi.hnqz.admin.api.constant.CacheConstants;
import com.qunzhixinxi.hnqz.admin.api.entity.SysDeptEncryptor;
import com.qunzhixinxi.hnqz.admin.entity.PartyAEncryptedInputRecord;
import com.qunzhixinxi.hnqz.admin.entity.PartyBEncryptedOutputRecord;
import com.qunzhixinxi.hnqz.admin.entity.dto.EncryptedInput;
import com.qunzhixinxi.hnqz.admin.entity.dto.EncryptedOutput;
import com.qunzhixinxi.hnqz.admin.mapper.PartyAEncryptedInputRecordMapper;
import com.qunzhixinxi.hnqz.admin.mapper.PartyBEncryptedOutputRecordMapper;
import com.qunzhixinxi.hnqz.admin.mapper.SysDeptEncryptorMapper;
import com.qunzhixinxi.hnqz.admin.util.AESUtils;
import com.qunzhixinxi.hnqz.admin.util.RSAUtils;
import com.qunzhixinxi.hnqz.common.core.util.R;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
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.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * {@code PartyAAspect} 甲方服务通知
 * <p>
 * 1.用于参数校验
 * 2.用户入参的记录
 * 3.用于解密入参
 * 4.用于加密响应结果
 * 5.用于记录发送响应信息
 *
 * @author Hengchen.Sun
 * @version 1.0.0
 * @date 2021-05-17
 * @since ver.1.0.0
 */
@AllArgsConstructor
@Aspect
@Component
@Slf4j
public class PartyAAspect {

	private final SysDeptEncryptorMapper sysDeptEncryptorMapper;
	private final RedisTemplate redisTemplate;
	private final PartyAEncryptedInputRecordMapper partyAEncryptedInputRecordMapper;
	private final PartyBEncryptedOutputRecordMapper partyBEncryptedOutputRecordMapper;

	@Pointcut("execution(* com.qunzhixinxi.hnqz.admin.controller.PartyAController.*(..))")
	public void controllerAspect() {
	}

	/**
	 * 统一处理甲方请求敏感数据的验签和解密
	 *
	 * @param proceedingJoinPoint 切入点
	 * @return 请求结果
	 * @throws Throwable
	 */
	@Around("controllerAspect()")
	public R doAroundControllerAspect(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

		// 获取参数
		HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
		Object[] args = proceedingJoinPoint.getArgs();
		EncryptedInput input = (EncryptedInput) args[0];

		// 信息展示
		log.info("===============请求内容===============");
		log.info("请求地址:" + request.getRequestURL());
		log.info("请求路径:" + request.getServletPath());
		log.info("请求方式:" + request.getMethod());
		log.info("请求类方法:" + proceedingJoinPoint.getSignature());
		log.info("请求类方法参数:" + JSONUtil.toJsonStr(input));
		log.info("===============请求内容===============");

		// 1. 获取私钥公钥摘要等
		SysDeptEncryptor deptEncryptor =
				sysDeptEncryptorMapper.selectOne(Wrappers.<SysDeptEncryptor>query()
						.eq("dept_id", Integer.parseInt(input.getAppId())));
		if (deptEncryptor == null || StringUtils.isEmpty(deptEncryptor.getCliPrivateKey()) || StringUtils.isEmpty(deptEncryptor
				.getCliPublicKey())
				|| StringUtils.isEmpty(deptEncryptor.getSerPrivateKey()) || StringUtils.isEmpty(deptEncryptor.getSerPublicKey())
				|| StringUtils.isEmpty(deptEncryptor.getSignature())) {
			return R.failed("结算加密器配置信息不全，请联系要易管理员");
		}
		input.setDeptEncryptor(deptEncryptor);

		// 1.1.请求记录
		PartyAEncryptedInputRecord inputRecord = new PartyAEncryptedInputRecord();
		BeanUtil.copyProperties(deptEncryptor, inputRecord);
		inputRecord.setCreateTime(LocalDateTime.now());
		inputRecord.setPassword(input.getPassword());
		inputRecord.setOriginalText(input.getEncryptedInfo());
		partyAEncryptedInputRecordMapper.insert(inputRecord);
		input.setInputId(inputRecord.getId());

		// 2. 验签
		try {
			if (!RSAUtils.isValid(deptEncryptor.getSignature(), input.getDigest(), deptEncryptor.getCliPublicKey())) {
				log.error("服务器公钥验签失败。摘要：{}，客户端公钥：{}。", input.getDigest(), deptEncryptor.getCliPublicKey());
				return R.failed("服务端公钥验签失败");
			}
		} catch (Exception e) {
			log.error("服务器公钥验签失败。摘要：{}，客户端公钥：{}。", input.getDigest(), deptEncryptor.getCliPublicKey());
			return R.failed("服务端公钥验签失败");
		}

		// 3.1 解密获取AES加密密码
		String password = input.getPassword();
		try {
			password = RSAUtils.decrypt(password, deptEncryptor.getSerPrivateKey());
		} catch (Exception e) {
			log.error("获取AES解密密码失败。密文：{}，服务器私钥：{}。", password, deptEncryptor.getSerPrivateKey());
			return R.failed("获取AES解密密码失败");
		}

		// 3.3 获取解密后的文档
		String decodeStr;
		try {
			decodeStr = AESUtils.decrypt(input.getEncryptedInfo(), password);
		} catch (Exception e) {
			log.error("AES解密失败。密文：{}，密码：{}。", input.getEncryptedInfo(), password);
			return R.failed("AES解密失败");
		}

		// 解密成功，替换参数，进行实际的业务操作
		input.setEncryptedInfo(decodeStr);
		args[0] = input;

		// 4.锁，针对不同的请求做不同的限制
		String info = tryLock(input.getAppId(), request.getServletPath(), decodeStr);
		if (StringUtils.isNotEmpty(info)) {
			log.error(info);
			return R.failed(info);
		}

		// 5.实际操作
		return (R) proceedingJoinPoint.proceed(args);

	}

	/**
	 * 统一修改响应结果
	 * 对结果进行数据加密
	 *
	 * @param joinPoint 接入点
	 * @param o         结果对象
	 * @throws Exception
	 */
	@AfterReturning(returning = "o", pointcut = "controllerAspect()")
	public void doAfterReturning(JoinPoint joinPoint, Object o) throws Exception {
		log.info("-------------->>> 处理应该数据，对敏感数据进行加密，开始 <<<----------------");
		log.info("Response内容:" + JSON.toJSONString(o));
		R result = (R) o;
		Map<String, String> resultMap = (Map<String, String>) result.getData();
		String msg = "2000".equals(resultMap.get("code")) ? resultMap.get("success_msg") : resultMap.get("error_msg");
		result.setMsg(msg);
		EncryptedInput input = (EncryptedInput) joinPoint.getArgs()[0];
		EncryptedOutput encryptedOutput = encapsulateOutput(resultMap, input.getDeptEncryptor());

		// 保存响应
		PartyBEncryptedOutputRecord outputRecord = new PartyBEncryptedOutputRecord();
		BeanUtil.copyProperties(input.getDeptEncryptor(), outputRecord);
		outputRecord.setCreateTime(LocalDateTime.now());
		outputRecord.setDeptId(Integer.parseInt(input.getAppId()));
		outputRecord.setPassword(encryptedOutput.getPassword());
		outputRecord.setOriginalText(encryptedOutput.getEncryptedInfo());
		outputRecord.setInputId(input.getInputId());
		partyBEncryptedOutputRecordMapper.insert(outputRecord);

		result.setData(encryptedOutput);
		log.info("-------------->>> 处理应该数据，对敏感数据进行加密, 结束 <<<----------------");
	}

	/**
	 * 封装响应信息
	 *
	 * @param result
	 * @param deptEncryptor
	 * @return
	 * @throws Exception
	 */
	private EncryptedOutput encapsulateOutput(Map<String, String> result, SysDeptEncryptor deptEncryptor) throws Exception {
		EncryptedOutput output = new EncryptedOutput();
		// 生成加密秘钥
		final String pw = RandomStringUtils.randomAlphanumeric(16);
		final String encryptedPassword = RSAUtils.encrypt(pw, deptEncryptor.getCliPublicKey());
		// 加密响应
		final String resultJson = JSONUtil.toJsonStr(result);
		final String encryptedOutputStr = AESUtils.encrypt(resultJson, pw);
		// 生成签名摘要
		final String outputDigest = RSAUtils.sign(deptEncryptor.getSignature(), deptEncryptor.getSerPrivateKey());

		output.setEncryptedInfo(encryptedOutputStr);
		output.setCharset("UTF-8");
		output.setFormat("JSON");
		output.setSignType("RSA");
		output.setTimestamp(LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli());
		output.setPassword(encryptedPassword);
		output.setDigest(outputDigest);
		return output;
	}

	/**
	 * 加锁
	 *
	 * @param appId       部门id
	 * @param servletPath 请求地址
	 * @param decodeStr   解密内容
	 * @return 如果正常返回null，否则返回报错信息
	 */
	private String tryLock(String appId, String servletPath, String decodeStr) {
		// 获取key
		JSONObject jsonObject = JSONUtil.isJsonArray(decodeStr) ? JSONUtil.parseObj(JSONUtil.parseArray(decodeStr)
				.get(0)) : JSONUtil.parseObj(decodeStr);
		String username = StringUtils.isNotEmpty(jsonObject.getStr("username")) ? jsonObject.getStr("username")
				: jsonObject.getStr("mobile");
		String key = CacheConstants.PARTY_A_CACHE_KEY + appId + ":" + username + ":" + servletPath;
		// 获取过期时间
		long expire = redisTemplate.opsForValue().getOperations().getExpire(key, TimeUnit.SECONDS);
		// 判断
		int period = 30;
		if ("/party-a/tax-helper/member".equals(servletPath)) {
			// 增员30内秒发起一次
			if (expire != -2) {
				return "增员操作30秒内只能发起一次。上次增员在处理，剩余时间为：" + expire + "秒！";
			}
		} else if ("/party-a/tax-helper/h5-token".equals(servletPath)) {
			// 三分钟内获取一次
			if (expire != -2) {
				return "H5Token，180秒内只能发起一次。剩余时间为：" + expire + "秒！";
			}
			period *= 6;
		} else if ("/party-a/tax-helper/binding-card-code".equals(servletPath)) {
			// 绑卡验证码1分钟内获取一次
			if (expire != -2) {
				return "绑卡验证码60秒内只能发起一次。剩余时间为：" + expire + "秒！";
			}
			period *= 2;
		} else if ("/party-a/tax-helper/ack-binding-card".equals(servletPath)) {
			// 确认绑卡30内秒发起一次
			if (expire != -2) {
				return "绑卡确认每30秒只能发起一次，剩余时间为：" + expire + "秒！";
			}
		}
		redisTemplate.opsForValue().set(key, decodeStr, period, TimeUnit.SECONDS);
		return null;
	}
}
