package com.sonic.common.log;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sonic.common.auth.AuthCaller;
import com.sonic.common.auth.AuthInterceptor;
import com.sonic.common.auth.OriginalCaller;
import com.sonic.common.auth.domains.Session;
import com.sonic.common.exception.BizException;
import com.sonic.common.rpc.Result;
import com.sonic.common.utils.IpAddressUtils;
import com.sonic.common.utils.LogUtils;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableSet;
import lombok.extern.slf4j.Slf4j;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 统一打印日志(支持忽略此aop, 采用@IgnoreRequestLog注解)
 * @author code
 */
@Aspect
@Slf4j
public class RequestLogAspect {
    private final static Integer MAX_LOG_SIZE = 2048;

    public static final String REQUEST_ATTR_SESSION = "_session_";


    private static Set<Class<?>> EXCLUDE_CLASSES = ImmutableSet.of(ServletRequest.class, ServletResponse.class,
            MultipartFile.class, AuthCaller.class, OriginalCaller.class);

    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping) " +
            "||@annotation(org.springframework.web.bind.annotation.PostMapping) " +
            "||@annotation(org.springframework.web.bind.annotation.DeleteMapping) " +
            "||@annotation(org.springframework.web.bind.annotation.PutMapping) " +
            "||@annotation(org.springframework.web.bind.annotation.GetMapping) ")
    public void pointCut() {
    }



    @Around("pointCut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {


        ServletRequestAttributes  servletRequestAttributes =     ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
        if(servletRequestAttributes ==null){
            // 这里需要排除 fegin调用的情况 RequestContextHolder.getRequestAttributes() 不在当前线程的情况下 会造成空指针异常 ，例如 定时任务或者其他开的线程。
            return  joinPoint.proceed(joinPoint.getArgs());
        }

        HttpServletRequest request = servletRequestAttributes.getRequest();
        String internalTraceId =  request.getHeader("X-TRACE-ID");

        if(StringUtils.isNotEmpty(internalTraceId)){
            LogUtils.setTraceId(internalTraceId);
        }else{
            LogUtils.setTraceId();
        }
        if (request != null) {
            //用于在AbstractExceptionHandler中为dumpRequest, AbstractExceptionHandler无法直接获取body
            request.setAttribute("params", this.getUserArgs(joinPoint.getArgs()));
        }

        IgnoreRequestLog annotation = AnnotationUtils.findAnnotation(((MethodSignature) joinPoint.getSignature()).getMethod(),
                IgnoreRequestLog.class);

        // 获取注解中的ignoreTypes
        ImmutableSet<IgnoreRequestLog.IgnoreType> ignoreTypes = Optional.ofNullable(annotation)
                .map(a -> ImmutableSet.copyOf(a.types())).orElse(ImmutableSet.of());

        Stopwatch stopwatch = Stopwatch.createStarted();
        Object caller = Optional.ofNullable(request.getAttribute(AuthInterceptor.CALLER_PAYLOAD_KEY))
                .map(JSON::toJSONString).orElse("UNKNOWN");
        Session session = (Session) request.getAttribute(REQUEST_ATTR_SESSION);
        String userId = session == null || session.getUserId() == null ? "UNKNOWN" : session.getUserId().toString();
        String requestLog = buildRequestLog(joinPoint, ignoreTypes);

        Object proceed;
        try {
            proceed = joinPoint.proceed(joinPoint.getArgs());
        } catch (BizException e) {
            //stein会收集error级别的异常并告警. 对BizException. 不期望收到告警邮件. 因此将BizException对应的日志级别调整为warn
            log.warn("api log, process error, caught BizException, ip = {}, url = {}, caller = {}, userId = {}, params = {}, " +
                            "time cost = {} ms, BizException:",
                    IpAddressUtils.getIpAddress(request), request.getRequestURI(), caller, userId, requestLog, stopwatch.elapsed(TimeUnit.MILLISECONDS), e);
            throw e;
        } catch (Exception e) {
            log.error("api log, process error, uncaught exception, ip = {}, url = {}, caller = {}, userId = {}, params = {}, " +
                            "time cost = {} ms, exception:",
                    IpAddressUtils.getIpAddress(request), request.getRequestURI(), caller, userId, requestLog, stopwatch.elapsed(TimeUnit.MILLISECONDS), e);
            throw e;
        }

        // 如果该接口定义的类型不是SILENT的, 则打印日志
        if (!ignoreTypes.contains(IgnoreRequestLog.IgnoreType.SILENT)) {
            //响应时间 > 1s 时输出具体响应的秒数日志  1 - 9s 为等级1、>= 10s 为等级2
            long timeCost = stopwatch.elapsed(TimeUnit.MILLISECONDS);
            log.info("api log, ip = {}, url = {}, caller = {}, params = {}, result = {}, time cost = {} ms{}",
                    IpAddressUtils.getIpAddress(request),
                    request.getRequestURI(),
                    caller,
                    requestLog,
                    buildResponseLog(proceed, ignoreTypes),
                    timeCost
            );
        }
        LogUtils.removeTraceId();
        return proceed;
    }

    private String buildRequestLog(ProceedingJoinPoint joinPoint, Set<IgnoreRequestLog.IgnoreType> ignoreTypes) {
        if (ignoreTypes.contains(IgnoreRequestLog.IgnoreType.REQUEST)) {
            return "IGNORED";
        }
        String jsonString = JSONObject.toJSONString(this.getUserArgs(joinPoint.getArgs()));
        return StringUtils.left(jsonString, MAX_LOG_SIZE);
    }

    private String buildResponseLog(Object proceed, Set<IgnoreRequestLog.IgnoreType> ignoreTypes) {
        if (proceed == null || !(Result.class.isAssignableFrom(proceed.getClass()))) {
            return StringUtils.EMPTY;
        }else{
            Result r =  (Result) proceed;
            ((Result) proceed).setTraceId(LogUtils.getTraceId());
        }
        if (ignoreTypes.contains(IgnoreRequestLog.IgnoreType.RESPONSE)) {
            return "IGNORED";
        }
        return StringUtils.left(JSONObject.toJSONString(proceed), MAX_LOG_SIZE);
    }

    protected List<Object> getUserArgs(Object[] args) {
        return Arrays.stream(args)
                .filter((p) ->
                        Objects.nonNull(p) && EXCLUDE_CLASSES.stream().noneMatch(clz -> clz.isAssignableFrom(p.getClass()))

                ).collect(Collectors.toList());
    }
}

