/*
 * Copyright [2020-2030] [https://www.stylefeng.cn]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Guns采用APACHE LICENSE 2.0开源协议，您在使用过程中，需要注意以下几点：
 *
 * 1.请不要删除和修改根目录下的LICENSE文件。
 * 2.请不要删除和修改Guns源码头部的版权声明。
 * 3.请保留源码和相关描述文件的项目出处，作者声明等。
 * 4.分发源码时候，请注明软件出处 https://gitee.com/stylefeng/guns
 * 5.在修改包名，模块名称，项目代码等时，请注明软件出处 https://gitee.com/stylefeng/guns
 * 6.若您的项目无法满足以上几点，可申请商业授权
 */
package cn.stylefeng.roses.kernel.log.requestapi.aop;

import cn.hutool.core.util.ObjectUtil;
import cn.stylefeng.roses.kernel.auth.api.context.LoginContext;
import cn.stylefeng.roses.kernel.auth.api.pojo.login.LoginUser;
import cn.stylefeng.roses.kernel.log.api.constants.LogFileConstants;
import cn.stylefeng.roses.kernel.log.api.expander.LogConfigExpander;
import cn.stylefeng.roses.kernel.log.api.factory.LogRecordFactory;
import cn.stylefeng.roses.kernel.log.api.factory.appender.AuthedLogAppender;
import cn.stylefeng.roses.kernel.log.api.factory.appender.HttpLogAppender;
import cn.stylefeng.roses.kernel.log.api.factory.appender.ParamsLogAppender;
import cn.stylefeng.roses.kernel.log.api.pojo.record.LogRecordDTO;
import cn.stylefeng.roses.kernel.log.requestapi.LogRecordApi;
import cn.stylefeng.roses.kernel.rule.annotation.ApiLog;
import cn.stylefeng.roses.kernel.scanner.api.annotation.ApiResource;
import cn.stylefeng.roses.kernel.scanner.api.annotation.GetResource;
import cn.stylefeng.roses.kernel.scanner.api.annotation.PostResource;
import lombok.extern.slf4j.Slf4j;
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.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.ui.Model;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 每个请求接口记录日志的AOP
 * <p>
 * 将控制器controller包下的所有控制器类，执行的时候对url，参数，结果等进行记录
 *
 * @author fengshuonan
 * @since 2020/10/28 17:06
 */
@Aspect
@Slf4j
public class RequestApiLogRecordAop implements Ordered {

    /**
     * 日志记录的api
     */
    private final LogRecordApi logRecordApi;

    public RequestApiLogRecordAop(LogRecordApi logRecordApi) {
        this.logRecordApi = logRecordApi;
    }

    /**
     * 切所有controller包
     */
    @Pointcut("execution(* *..controller.*.*(..)) || execution(* *..*Controller.*(..))")
    public void cutService() {
    }

    @Around("cutService()")
    public Object aroundPost(ProceedingJoinPoint point) throws Throwable {
        Object result = point.proceed();

        try {
            // 判断是否需要记录日志
            boolean ensureMakeLog = this.ensureMakeLog(point);
            if (!ensureMakeLog) {
                return result;
            }

            if (ObjectUtil.isNotEmpty(result)) {
                // 获取接口上@PostResource或者@GetResource的name属性和requiredLogin属性
                Map<String, Object> annotationProp = getAnnotationProp(point);

                // 获取字段的名
                Map<String, Object> args = getFieldsName(point);

                // 删除Request和Response
                args.entrySet().removeIf(objectEntry -> objectEntry.getValue() instanceof HttpServletRequest || objectEntry.getValue() instanceof HttpServletResponse);

                // 记录日志
                recordLog(args, result, annotationProp);
            }
        } catch (Exception e) {
            log.error("日志记录没有记录成功！", e);
        }

        return result;
    }

    /**
     * AOP获取 @PostResource 和 @GetResource 属性信息
     *
     * @param joinPoint joinPoint对象
     * @return 返回K, V格式的参数，key是参数名称，v是参数值
     * @author liuhanqing
     * @since 2020/12/22 21:18
     */
    private Map<String, Object> getAnnotationProp(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();

        // 通过map封装参数和参数值，key参数名，value是参数值
        Map<String, Object> propMap = new HashMap<>(2);
        String name = null;
        String requiredLogin = null;

        // 获取接口上的@PostResource或者@GetResource的name属性和requiredLogin属性，填充到map
        ApiResource apiResource = method.getAnnotation(ApiResource.class);
        GetResource getResource = method.getAnnotation(GetResource.class);
        PostResource postResource = method.getAnnotation(PostResource.class);

        if (apiResource != null) {
            propMap.put("name", apiResource.name());
            propMap.put("requiredLogin", apiResource.requiredLogin());
        }

        if (getResource != null) {
            propMap.put("name", getResource.name());
            propMap.put("requiredLogin", getResource.requiredLogin());
        }

        if (postResource != null) {
            propMap.put("name", postResource.name());
            propMap.put("requiredLogin", postResource.requiredLogin());
        }

        return propMap;
    }

    /**
     * 将请求方法记录日志的过程
     *
     * @param params         AOP拦截方法的参数封装，key是参数名称，v是参数值
     * @param result         AOP拦截方法的返回值
     * @param annotationProp AOP拦截注解属性
     * @author fengshuonan
     * @since 2020/10/28 17:38
     */
    private void recordLog(Map<String, Object> params, Object result, Map<String, Object> annotationProp) {

        Object actionName = annotationProp.get("name");

        // 创建日志对象
        LogRecordDTO logRecordDTO = LogRecordFactory.createLogRecord(actionName);

        // 填充用户登录信息
        LoginUser loginUserNullable = LoginContext.me().getLoginUserNullable();
        if (loginUserNullable != null) {
            AuthedLogAppender.appendAuthedHttpLog(logRecordDTO, loginUserNullable.getToken(), loginUserNullable.getUserId());
        }

        // 填充http接口请求信息
        HttpLogAppender.appendHttpLog(logRecordDTO);

        // 追加参数信息
        ParamsLogAppender.appendAuthedHttpLog(logRecordDTO, params, result);

        // 异步记录日志
        logRecordApi.addAsync(logRecordDTO);
    }

    @Override
    public int getOrder() {
        return LogFileConstants.DEFAULT_API_LOG_AOP_SORT;
    }

    /**
     * AOP获取参数名称和参数值
     *
     * @param joinPoint joinPoint对象
     * @return 返回K, V格式的参数，key是参数名称，v是参数值
     * @author majianguo
     * @since 2020/11/2 10:40
     */
    private Map<String, Object> getFieldsName(ProceedingJoinPoint joinPoint) {

        // 获取被拦截方法的所有参数
        Object[] args = joinPoint.getArgs();

        // 通过map封装参数和参数值，key参数名，value是参数值
        Map<String, Object> paramMap = new HashMap<>(args.length);
        try {
            String classType = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();

            // 处理基本类型
            Class<?>[] classes = new Class[args.length];
            for (int k = 0; k < args.length; k++) {
                if (args[k] instanceof Model) {
                    classes[k] = Model.class;
                } else if (args[k] instanceof MultipartFile) {
                    classes[k] = MultipartFile.class;
                } else {
                    classes[k] = args[k].getClass();
                }

            }

            ParameterNameDiscoverer defaultParameterNameDiscoverer = new DefaultParameterNameDiscoverer();

            // 获取指定的方法，第二个参数可以不传，但是为了防止有重载的现象，还是需要传入参数的类型
            Method method = Class.forName(classType).getMethod(methodName, classes);

            // 参数名
            String[] parameterNames = defaultParameterNameDiscoverer.getParameterNames(method);

            // 为空直接返回
            if (null == parameterNames) {
                return new HashMap<>(1);
            }

            // 装载参数名称和参数值
            for (int i = 0; i < parameterNames.length; i++) {
                // 解决上传文件接口aop记录日志报错
                if (args[i] instanceof MultipartFile && args[i] != null) {
                    // 根据参数名只记录文件名
                    paramMap.put(parameterNames[i], ((MultipartFile) args[i]).getOriginalFilename());
                } else {
                    paramMap.put(parameterNames[i], args[i]);
                }

            }

        } catch (Exception e) {

            // 打印日志
            if (log.isDebugEnabled()) {
                e.printStackTrace();
            }

            // 有异常则不显示参数名称直接返回参数
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof MultipartFile && args[i] != null) {
                    // 根据参数名只记录文件名
                    paramMap.put("args" + i, ((MultipartFile) args[i]).getOriginalFilename());
                } else {
                    paramMap.put("args" + i, args[i]);
                }
            }

        }

        return paramMap;
    }

    /**
     * 确定当前接口是否需要记录日志
     *
     * @author fengshuonan
     * @since 2022/1/12 20:44
     */
    private boolean ensureMakeLog(ProceedingJoinPoint point) {
        // 判断是否需要记录日志，如果不需要直接返回
        Boolean openFlag = LogConfigExpander.getGlobalControllerOpenFlag();

        // 获取类上的业务日志开关注解
        Class<?> controllerClass = point.getTarget().getClass();
        ApiLog businessLog = controllerClass.getAnnotation(ApiLog.class);

        // 获取方法上的业务日志开关注解
        ApiLog methodBusinessLog = null;
        MethodSignature methodSignature = null;
        if (!(point.getSignature() instanceof MethodSignature)) {
            return false;
        }
        methodSignature = (MethodSignature) point.getSignature();
        Object target = point.getTarget();
        try {
            Method currentMethod = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
            methodBusinessLog = currentMethod.getAnnotation(ApiLog.class);
        } catch (NoSuchMethodException e) {
            return false;
        }

        // 如果开关开启
        if (openFlag) {
            // 如果控制器类上特意标明不做日志，则不记录日志
            if (businessLog != null && !businessLog.openLog()) {
                return false;
            }
            // 如果方法上标明不记录日志，则不记录日志
            return methodBusinessLog == null || methodBusinessLog.openLog();
        } else {
            // 如果全局开关没开启，但是类上有特殊标记开启日志，则以类上标注为准
            if (businessLog != null && businessLog.openLog()) {
                return true;
            }
            // 如果方法上标明不记录日志，则不记录日志
            return methodBusinessLog != null && methodBusinessLog.openLog();
        }
    }

}
