package com.iformall.log;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.iformall.common.LogColumn;
import com.iformall.domain.po.invest.InvestBaseEntity;
import com.iformall.domain.po.invest.InvestOperateRecordEntity;
import com.iformall.domain.vo.invest.TableTriple;
import com.iformall.enums.EnumTableOperateType;
import com.iformall.service.invest.InvestHelper;
import com.iformall.service.invest.InvestOperateRecordService;
import com.iformall.service.invest.impl.InvestBaseServiceImpl;
import com.iformall.utils.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.compress.utils.Lists;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.*;

/**
 *
 */
@Slf4j
@Aspect
@Component
public class TableOperateAspect {

	@Autowired
	private InvestOperateRecordService operateRecordService;

	@Pointcut("@annotation(com.iformall.common.TableLog)")
	public void pointcut() {
	}

	@Around("pointcut()")
	public Object round(ProceedingJoinPoint joinPoint) {
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		log.debug("before , {}.{}() with argument[s] = {}", signature.getDeclaringType(),
				signature.getName(), Arrays.toString(joinPoint.getArgs()));
		Object result = null;
		Object entity = null;
		Map<String, Object> beforeMap = null;
		Long tid = null;
		try {
			entity = joinPoint.getArgs()[0];
			tid = getId(entity);
			beforeMap = getFeildMap(getById(signature, tid));
		} catch (Exception e) {
			log.error("before ", e);
		}
		try {
			result = joinPoint.proceed();
		} catch (Throwable throwable) {
			log.error("proceed error , Illegal argument: {} in {}.{}()", Arrays.toString(joinPoint.getArgs()),
					joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(), throwable);
		} finally {
			log.debug("after , result: {}", result);
		}
		try {
			saveRecord(signature, beforeMap, entity, tid);
		} catch (Exception e) {
			log.error("save table Record ", e);
		}
		return result;
	}

	private Long getId(Object entity) {
		Long tid;
		if (entity instanceof InvestBaseEntity) {
			tid = ((InvestBaseEntity) entity).getId();
		} else {
			tid = (Long) entity;
		}
		return tid;
	}

	private void saveRecord(MethodSignature signature, Map<String, Object> beforeMap, Object arg, Long tid) throws IllegalAccessException {
		EnumTableOperateType operateType = getOperateType(signature.getName());
		if (Objects.equals(operateType, EnumTableOperateType.DELETE)) {
			arg = null;
		}
		if (Objects.isNull(tid)) {
			tid = getId(arg);
		}
		Map<String, Object> afterMap = getFeildMap(arg);
		List content = getContent(beforeMap, afterMap);
		log.debug("operate content: {}", JSON.toJSONString(content));
		InvestOperateRecordEntity recordEntity = new InvestOperateRecordEntity();
		if (CollectionUtils.isEmpty(content)) {
			return;
		}
		recordEntity.setContent(JSON.toJSONString(content));
		recordEntity.setOperateType(operateType);
		recordEntity.setTbl(getTbl(signature));
		recordEntity.setTid(tid);
		operateRecordService.save(recordEntity);
		log.debug("saveRecord: {}", JSON.toJSONString(recordEntity));
	}

	private String getTbl(MethodSignature signature) {
		ParameterizedType type = (ParameterizedType) signature.getDeclaringType().getGenericSuperclass();
		Class clazz = (Class) type.getActualTypeArguments()[1];
		TableName tableName = (TableName) clazz.getAnnotation(TableName.class);
		return tableName.value();
	}

	private EnumTableOperateType getOperateType(String methodName) {
		if (StringUtils.contains(methodName, "update")) {
			return EnumTableOperateType.UPDATE;
		} else if (StringUtils.contains(methodName, "remove")) {
			return EnumTableOperateType.DELETE;
		}
		return EnumTableOperateType.ADD;
	}

	private Map<String, Object> getFeildMap(Object entity) throws IllegalAccessException {
		if (Objects.isNull(entity)) {
			return null;
		}
		Map<String, Object> map = new HashMap<>();
		List<Field> fields = FieldUtils.getFieldsListWithAnnotation(entity.getClass(), LogColumn.class);
		for (Field field : fields) {
			String annotationVal = field.getAnnotation(LogColumn.class).value();
			Object value = FieldUtils.readDeclaredField(entity, field.getName(), true);
			if (field.getType().isEnum()) {
				value = InvestHelper.getEnumVal(InvestHelper.valueOf(value.getClass(), ((Enum) value).name()));
			}
			if (Objects.equals(annotationVal, InvestBaseEntity.COLUMN_TYPE)) {
				value = JSON.parseObject((String) value, Map.class);
			}
			map.put(field.getName(), value);
		}
		return map;
	}

	private List<TableTriple<String, Object, Object>> getContent(Map<String, Object> before, Map<String, Object> after) {
		if (Objects.isNull(before) || Objects.isNull(after)) {
			return mapToList(before, after);
		}
		MapDifference<String, Object> diffInner = Maps.difference(before, after);
		List<TableTriple<String, Object, Object>> diffRows = Lists.newArrayList();
		if (diffInner.areEqual()) {
			return diffRows;
		}
		Map<String, MapDifference.ValueDifference<Object>> differenceMap = diffInner.entriesDiffering();
		for (Map.Entry<String, MapDifference.ValueDifference<Object>> entry : differenceMap.entrySet()) {
			String s = entry.getKey();
			MapDifference.ValueDifference<Object> objectValueDifference = entry.getValue();
			Object innerBefore = objectValueDifference.leftValue();
			Object innerAfter = objectValueDifference.rightValue();
			if (innerBefore instanceof Map) {
				diffRows.addAll(getContent((Map<String, Object>) innerBefore, (Map<String, Object>) innerAfter));
			} else {
				diffRows.add(new TableTriple(s, innerBefore, innerAfter));
			}
		}
		return diffRows;
	}

	private List<TableTriple<String, Object, Object>> mapToList(Map<String, Object> before, Map<String, Object> after) {
		List<TableTriple<String, Object, Object>> list = Lists.newArrayList();
		Set<String> keys = Objects.isNull(before) ? after.keySet() : before.keySet();
		for (String key : keys) {
			if (Objects.isNull(before)) {
				Object innerAfter = after.get(key);
				if (innerAfter instanceof Map) {
					list.addAll(mapToList(null, (Map<String, Object>) innerAfter));
				} else {
					list.add(new TableTriple(key, "", innerAfter));
				}
			} else {
				Object innerBefore = before.get(key);
				if (innerBefore instanceof Map) {
					list.addAll(mapToList((Map<String, Object>) innerBefore, null));
				} else {
					list.add(new TableTriple(key, innerBefore, ""));
				}
			}
		}
		return list;
	}

	private <T> T getById(MethodSignature signature, Serializable id) {
		Class service = signature.getDeclaringType();
		QueryWrapper<T> wrapper = new QueryWrapper<>();
		wrapper.eq("id", id);
		return (T) ((InvestBaseServiceImpl) SpringContextUtils.getBean(service)).getOne(wrapper);
	}
}
