package com.arms.api.util.aspect;

import com.arms.api.util.slack.SlackNotificationService;
import com.arms.api.util.slack.SlackProperty;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.RequestFacade;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpSession;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Slf4j
@Aspect
@Component
public class LoggingAdvice {

    private final SlackNotificationService slackNotificationService;
    private final String appName;
    private final ObjectMapper objectMapper;

    @Autowired
    public LoggingAdvice(SlackNotificationService slackNotificationService,
                         @Value("${spring.application.name}") String appName,
                         ObjectMapper objectMapper) {
        this.slackNotificationService = slackNotificationService;
        this.appName = appName;
        this.objectMapper = objectMapper;
        this.objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }

    @Pointcut("execution(* com.arms..controller.*.*(..))")
    public void controller() {
    }

    @Pointcut("execution(* com.arms..service.*.*(..))")
    public void service(){}

    @Before("controller() || service()")
    private void loggingBeforeControllerAndService(JoinPoint joinPoint) {
        String className = getClassName(joinPoint);
        String methodName = getMethodName(joinPoint);

        log.info("[ {} :: {} ] :: Start", className, methodName);
    }

    @AfterReturning("controller() || service()")
    private void loggingAfterSuccessControllerAndService(JoinPoint joinPoint) {
        String className = getClassName(joinPoint);
        String methodName = getMethodName(joinPoint);

        log.info("[ {} :: {} ] :: End", className, methodName);
    }

    @Around("controller() || service()")
    public Object errorLoggingAndNotifyingToSlack(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

        try {
            return proceedingJoinPoint.proceed();
        }
        catch (Exception ex) {

            slackNotificationService.sendMessageToChannel(SlackProperty.Channel.globalconfig, ex);

            Object[] args = proceedingJoinPoint.getArgs();
            MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
            String methodName = methodSignature.getMethod().getName();

            StringWriter errors = new StringWriter();
            ex.printStackTrace(new PrintWriter(errors));;

            List<Object> argsObject = Arrays.stream(args)
                    .filter(arg -> !(arg instanceof RequestFacade))
                    .collect(Collectors.toList());

            String sessionId = Arrays.stream(args)
                    .filter(arg -> arg instanceof RequestFacade)
                    .map(arg -> (RequestFacade) arg)
                    .map(RequestFacade::getSession)
                    .filter(Objects::nonNull)
                    .map(HttpSession::getId)
                    .findFirst()
                    .orElse("null");

            logErrorDetails(appName, methodName, sessionId, argsObject, errors.toString());

            throw ex;
        }

    }

    private String getClassName(JoinPoint joinPoint) {
        return joinPoint.getTarget().getClass().getSimpleName();
    }

    private String getMethodName(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        return methodSignature.getMethod().getName();
    }

    private String convertToJsonOrDefault(Object obj) {

        if (isPrimitiveOrWrapper(obj)) {
            return obj.toString();
        }

        return convertToJson(obj);
    }

    private boolean isPrimitiveOrWrapper(Object obj) {
        return obj.getClass().isPrimitive() ||
                obj instanceof Number ||
                obj instanceof Boolean ||
                obj instanceof Character ||
                obj instanceof String;
    }

    private String convertToJson(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        }
        catch (JsonProcessingException e) {
            log.error("Failed to convert object to JSON", e.getMessage());
            return obj.toString();
        }
    }

    private void logErrorDetails(String appName, String methodName, String sessionId, List<Object> argsObject, String errors) {
        argsObject.stream()
                .filter(Objects::nonNull)
                .forEach(arg -> {
                    try {
                        String paramType = arg != null ? arg.getClass().getSimpleName() : "null";
                        String paramString = arg != null ? convertToJsonOrDefault(arg) : "null";

                        StringBuilder sb = new StringBuilder();
                        sb.append("Error 발생\n")
                                .append("appName : ").append(appName).append("\n")
                                .append("methodName : ").append(methodName).append("\n")
                                .append("session : ").append(sessionId).append("\n")
                                .append("parameter type : ").append(paramType).append("\n")
                                .append("parameter value : ").append(paramString).append("\n")
                                .append("errorMsg : ").append(errors).append("\n");

                        log.error(sb.toString());
                    }
                    catch(Exception e) {
                        log.error("logErrorDetails Exception : " + e.getMessage(), e);
                    }
                });
    }
}