package cn.lihongjie.coal.aop;

import cn.lihongjie.coal.annotation.SysLog;
import cn.lihongjie.coal.base.dto.CommonDto;
import cn.lihongjie.coal.base.entity.CommonEntity;
import cn.lihongjie.coal.common.RequestUtils;
import cn.lihongjie.coal.ip.IpQueryService;
import cn.lihongjie.coal.session.SessionService;
import cn.lihongjie.coal.syslog.entity.SysLogEntity;
import cn.lihongjie.coal.syslog.service.SysLogService;

import jakarta.servlet.http.HttpServletRequest;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
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.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;
import java.util.Arrays;

@Aspect
@Component
@Slf4j
@Order(value = 100)
public class ControllerAop {

    @Autowired SessionService sessionService;
    @Autowired IpQueryService ipQueryService;
    @Autowired SysLogService sysLogService;
    private final SpelParserConfiguration config;
    private final ExpressionParser parser;

    private static void updateSysLog(Throwable e, SysLogEntity sysLogEntity) {
        if (sysLogEntity != null) {
            sysLogEntity.setOptStatus("1");
            sysLogEntity.setStacktrace(ExceptionUtils.getStackTrace(e));
        }
    }

    public ControllerAop() {
        config = new SpelParserConfiguration(true, true);

        parser = new SpelExpressionParser(config);
    }

    @Pointcut("execution (* cn.lihongjie.coal.*.controller.*.*(..))")
    public void controllerMethods() {}

    @SneakyThrows
    @Around("controllerMethods()")
    @Transactional
    public Object call(ProceedingJoinPoint proceedingJoinPoint) {

        Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
        HttpServletRequest request =
                ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                        .getRequest();

        long start = System.currentTimeMillis();
        SysLogEntity sysLogEntity = createSysLog(method, request, proceedingJoinPoint.getArgs());
        try {

            return proceedingJoinPoint.proceed();

        } catch (Throwable e) {

            logException(e, proceedingJoinPoint);

            updateSysLog(e, sysLogEntity);

            throw e;

        } finally {

            saveSysLog(sysLogEntity, System.currentTimeMillis(), start);
        }
    }

    private void saveSysLog(SysLogEntity sysLogEntity, long end, long start) {
        if (sysLogEntity != null) {
            sysLogEntity.setTimeCost((int) (end - start));

            sysLogService.saveSync(sysLogEntity);
        }
    }

    private SysLogEntity createSysLog(Method method, HttpServletRequest request, Object[] args) {
        SysLog sysLog = AnnotationUtils.findAnnotation(method, SysLog.class);

        SysLogEntity sysLogEntity = null;
        String module = "";
        if (sysLog != null) {

            module = sysLog.module();
            if (StringUtils.isEmpty(module)) {
                SysLog classLog =
                        AnnotationUtils.findAnnotation(method.getDeclaringClass(), SysLog.class);
                if (classLog != null) {

                    module = classLog.module();
                }
            }

            sysLogEntity = new SysLogEntity();
            sysLogEntity.setModule(module);
            sysLogEntity.setMessage(buildMessage(sysLog, method, args, request));
            sysLogEntity.setAction(sysLog.action());
            sysLogEntity.setIp(RequestUtils.getIp(request));
            sysLogEntity.setIpLocation(ipQueryService.query(sysLogEntity.getIp()));
            sysLogEntity.setUrl(request.getRequestURI());
            sysLogEntity.setOptStatus("0");
            sysLogEntity.setTimeCost(0);
            sysLogEntity.setUserAgent(RequestUtils.getUa(request));
        }
        return sysLogEntity;
    }

    private String buildMessage(
            SysLog sysLog, Method method, Object[] args, HttpServletRequest request) {
        String message = sysLog.message();
        if (StringUtils.isEmpty(sysLog.message())) {
            if (args != null
                    && args.length == 1
                    && args[0] != null
                    && (args[0] instanceof CommonDto || args[0] instanceof CommonEntity)) {

                message = "(id?:'') + ' ' + (name?:'') + ' ' + (code?:'')";
            } else {

                message = "";
            }
        }

        try {

            if (StringUtils.isNotBlank(message)) {

                MethodBasedEvaluationContext context =
                        new MethodBasedEvaluationContext(
                                ArrayUtils.isEmpty(args) ? null : args[0],
                                method,
                                args,
                                new DefaultParameterNameDiscoverer());
                context.setVariable("request", request);
                Object value = parser.parseExpression(message).getValue(context);
                return value + "";
            } else {
                return "";
            }
        } catch (Exception e) {
            log.error("解析日志表达式失败: {}", sysLog.message(), e);
            return "";
        }
    }

    private void logException(Throwable ex, ProceedingJoinPoint proceedingJoinPoint) {
        HttpServletRequest request =
                ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                        .getRequest();

        Object[] args = proceedingJoinPoint.getArgs();
        Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();

        log.info(
                "接口调用异常: {}\nurl:{} {}\nmethod: {}\nargs: {}",
                ex.getMessage() == null ? "no message" : ex.getMessage(),
                request.getMethod(),
                request.getRequestURL(),
                method,
                Arrays.toString(args),
                ex);

        request.setAttribute("__logged", true);
    }
}
