package com.lcrx.selector.aop.implement;

import com.lcrx.selector.aop.definition.BusinessLog;
import com.lcrx.selector.aop.annotation.Module;
import com.lcrx.selector.enums.LogType;
import com.lcrx.selector.repository.BusinessLogRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;

import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

/**
 * @author ###
 */
@Slf4j
@Aspect
@Component
public class BusinessLogAOP {

    private final static String REQUEST_METHOD_GET = "GET";
    private final BusinessLogger businessLogger;
    private final HttpServletRequest request;
    private final BusinessLogData businessLogData;
    private static final ThreadLocal<Long> startTime = new ThreadLocal<>();
    public static final String USER_CODE = "system";

    @Autowired
    public BusinessLogAOP(BusinessLogger businessLogger,
                          HttpServletRequest request,
                          BusinessLogData businessLogData
                          ) {
        this.businessLogger = businessLogger;
        this.request = request;
        this.businessLogData = businessLogData;
    }

    @Around(value = "@annotation(com.lcrx.selector.aop.definition.BusinessLog)")
    public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        startTime.set(System.currentTimeMillis());
        Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
        BusinessLog businessLogAnnotation = method.getAnnotation(BusinessLog.class);

        String module = getModule(businessLogAnnotation, proceedingJoinPoint);
        businessLogData.setOperator(request.getHeader(USER_CODE));
        if (REQUEST_METHOD_GET.equals(request.getMethod())) {
            recordGetRequest(businessLogAnnotation, module);
        } else {
            recordNonGetRequest(businessLogAnnotation, proceedingJoinPoint, method, module);
        }

        recordHeaderContext();

        return proceedingJoinPoint.proceed();
    }

    @AfterReturning(value = "@annotation(com.lcrx.selector.aop.definition.BusinessLog)", returning = "response")
    public void afterReturn(Object response) {
        try {
            businessLogData
            .setTook(System.currentTimeMillis() - startTime.get())
            .setResponse(response);
            businessLogger.log(businessLogData);
        } catch (Exception e) {
            log.error("Record response biz log error.", e);
        }
    }

    private void recordHeaderContext(){
        Enumeration<String> enumeration = request.getHeaderNames();
        StringBuffer headers = new StringBuffer();

        while (enumeration.hasMoreElements()) {
            String name = enumeration.nextElement();
            String value = request.getHeader(name);
            headers.append(name + ":" + value).append(",");
        }

        businessLogData.setHeaderContext(headers.toString());
    }

    private int calculateSize(final Object o){
        if (o == null) {
            return 0;
        }
        ByteArrayOutputStream buf = new ByteArrayOutputStream(4096);
        ObjectOutputStream out = null;
        try{
            out = new ObjectOutputStream(buf);
            out.writeObject(o);
            out.flush();
            buf.close();
        }catch (IOException e){
            log.error("calculate object size fail.");
        }
        return buf.size();
    }

    private String getModule(BusinessLog businessLogAnnotation, ProceedingJoinPoint pjp) {

        String module = businessLogAnnotation.module();

        if (StringUtils.isBlank(module)) {

            Class<?> classTarget = pjp.getTarget().getClass();
            module = Optional.ofNullable(classTarget.getAnnotation(Module.class))
                    .map(Module::value).orElse("");
        }

        return module;
    }

    private void recordGetRequest(BusinessLog businessLogAnnotation, String module) {

        String requestUrl = buildGetRequestUrl();

        businessLogData.setLogType(LogType.API)
                .activate()
                .setAction(businessLogAnnotation.action())
                .setRequestMethod(request.getMethod())
                .setUri(request.getRequestURI())
                .setUrl(requestUrl)
                .setRemark(businessLogAnnotation.remark())
                .setModule(module);

        if(StringUtils.isBlank(businessLogData.getOperator())){
            businessLogData.setOperator(businessLogAnnotation.operator());
        }
    }

    private void recordNonGetRequest(BusinessLog businessLogAnnotation, ProceedingJoinPoint pjp, Method method, String module) {

        Annotation[][] parameterAnnotations = method.getParameterAnnotations();

        OptionalInt payloadIndex = IntStream.range(0, parameterAnnotations.length).filter(i ->

                Arrays.stream(parameterAnnotations[i]).filter(annotation ->
                        RequestBody.class.equals(annotation.annotationType())
                ).anyMatch(Objects::nonNull)

        ).findFirst();

        if (payloadIndex.isPresent()) {

            Object[] args = pjp.getArgs();

            Object payload = args[payloadIndex.getAsInt()];

            businessLogData.setPayload(payload);
        }

        businessLogData.setLogType(LogType.API)
                .activate()
                .setAction(businessLogAnnotation.action())
                .setRequestMethod(request.getMethod())
                .setUri(request.getRequestURI())
                .setUrl(request.getRequestURL().toString())
                .setRemark(businessLogAnnotation.remark())
                .setModule(module);

        if(StringUtils.isBlank(businessLogData.getOperator())){
            businessLogData.setOperator(businessLogAnnotation.operator());
        }
    }

    private String buildGetRequestUrl() {

        StringBuffer urlBuffer = request.getRequestURL();

        urlBuffer.append("?");

        Map<String, String[]> parameterMap = request.getParameterMap();

        AtomicInteger mark = new AtomicInteger(1);
        int size = parameterMap.size();

        parameterMap.entrySet().stream().forEach(entry -> {
            urlBuffer.append(entry.getKey()).append("=");
            String[] values = entry.getValue();
            IntStream.range(0, values.length).forEach(index -> {
                if (index > 0) {
                    urlBuffer.append(",");
                }
                urlBuffer.append(values[index]);
            });

            if (mark.get() < size) {
                mark.getAndIncrement();
                urlBuffer.append("&");
            }
        });

        return urlBuffer.toString();
    }
}
