package com.eactive.eai.rms.common.logging;

import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.ModelAndView;

import com.eactive.eai.rms.common.logging.RequestReportStopWatch.TaskInfo;

import lombok.extern.slf4j.Slf4j;

@Aspect("percflow(doFilter())")
@Slf4j
public class RequestReportAspect {

	private static final char LINE_SEPARATOR = '\n';
	private static final String BANNER_START = "»»ᅳEᅳᅳMᅳᅳSᅳ ᅳRᅳᅳEᅳᅳQᅳᅳUᅳᅳEᅳᅳSᅳᅳTᅳ ᅳRᅳᅳEᅳᅳPᅳᅳOᅳᅳRᅳᅳTᅳ ᅳSᅳᅳTᅳᅳAᅳᅳRᅳᅳTᅳ►";
	private static final String BANNER_END = "»»ᅳEᅳᅳMᅳᅳSᅳ ᅳRᅳᅳEᅳᅳQᅳᅳUᅳᅳEᅳᅳSᅳᅳTᅳ ᅳRᅳᅳEᅳᅳPᅳᅳOᅳᅳRᅳᅳTᅳ ᅳEᅳᅳNᅳᅳDᅳ►";

	private static final int LOG_LIMIT_COUNT = 1;

	protected Logger requestMappingLogger;
	private StringBuilder logBuilder;
	private StringBuilder indent;
	private RequestReportStopWatch requestReportStopWatch;

	@Pointcut("call (* javax.servlet.FilterChain.doFilter(..)) "
			+ " && within(com.eactive.eai.rms.common.filter.MonitoringFrontFilter)")
	public void doFilter() {

	}

	@Pointcut("execution(@org.springframework.web.bind.annotation.RequestMapping * com.eactive.eai.rms..*.*(..)) "
			+ " || execution(@org.springframework.web.bind.annotation.GetMapping * com.eactive.eai.rms..*.*(..)) "
			+ " || execution(@org.springframework.web.bind.annotation.PatchMapping * com.eactive.eai.rms..*.*(..)) "
			+ " || execution(@org.springframework.web.bind.annotation.PostMapping * com.eactive.eai.rms..*.*(..)) "
			+ " || execution(@org.springframework.web.bind.annotation.PutMapping * com.eactive.eai.rms..*.*(..)) ")
	public void controllerPointcut() {
		// pointcut
	}

	@Pointcut("(@within(org.springframework.stereotype.Component)        "
			+ "|| @within(org.springframework.stereotype.Service)        "
			+ "|| @within(org.springframework.stereotype.Repository)     "
			+ "|| within(com.eactive.eai.data.jpa.AbstractDataService+)     "
			+ "|| within(com.eactive.eai.rms.common.base.BaseService+)   "
			+ ")                                                         "
			+ "&& !within(com.eactive.eai.rms.common.interceptor..*)     "
			+ "&& !within(com.eactive.eai.rms.common.logging..*)         "
			+ "&& !within(com.eactive.eai.rms.common.datasource..*)      "
			+ "&& !within(com.eactive.eai.rms.data.config..*)             "
			+ "&& !execution(String com.eactive.eai.rms.data.entity.man.menu.MenuService.buildPositionString(..)) "
			+ "&& cflowbelow(doFilter())                                 "
			+ "&& (                                                      "
			+ " execution(* com.eactive.eai.rms..*.*(..))                "
			+ "|| call(* com.eactive.eai.data.BaseRepository+.*(..)))     ")
	public void cflowbelowPointcut() {
		// pointcut
	}

	@Before("doFilter()")
	public void beforeDoFilter(JoinPoint joinPoint) {

		if (!log.isTraceEnabled()) {
			return;
		}

		logBuilder = new StringBuilder();
		logBuilder.append(LINE_SEPARATOR);
		logBuilder.append(BANNER_START).append(LINE_SEPARATOR);
		indent = new StringBuilder();

		if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0
				&& joinPoint.getArgs()[0] instanceof HttpServletRequest) {
			requestParameterLogging((HttpServletRequest) joinPoint.getArgs()[0]);
		}

		requestReportStopWatch = new RequestReportStopWatch(joinPoint.getSignature().toString());
		requestReportStopWatch.start();
	}

	@Before("controllerPointcut()")
	public void beforeControllerPointcut(JoinPoint joinPoint) {

		if (!log.isTraceEnabled()) {
			return;
		}

		try {
			requestMappingLogger = LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringType());

			String taskInfoId = indent.toString() + joinPoint.getSignature().toString();
			TaskInfo taskInfo = requestReportStopWatch.getTaskInfoMap().get(taskInfoId);

			if (taskInfo == null || taskInfo.getStartCount() < LOG_LIMIT_COUNT) {
				logBuilder
						.append(indent)
						.append(joinPoint.getSignature().toString())
						.append(" : start time : ")
						.append(requestReportStopWatch.getTime());
				argumentLogging(joinPoint);
				logBuilder.append(LINE_SEPARATOR);
			} else if (taskInfo.getStartCount() < LOG_LIMIT_COUNT + 1) {
				logBuilder.append(indent).append("...\n");
			}

			requestReportStopWatch.startTask(taskInfoId);
			logBuilder.append(LINE_SEPARATOR);
		} catch (Throwable e) {
			log.trace("", e);
		}
	}

	private void requestParameterLogging(HttpServletRequest request) {
		logBuilder.append(request.getRequestURI()).append(" start!!");

		logBuilder.append(LINE_SEPARATOR).append("request parameters : ");

		for (@SuppressWarnings("rawtypes")
		Enumeration enu = request.getParameterNames(); enu.hasMoreElements();) {
			String parameterName = (String) enu.nextElement();
			logBuilder.append("{");
			logBuilder.append(parameterName + "=" + request.getParameter(parameterName));
			logBuilder.append("} ,");
		}
		logBuilder.append(LINE_SEPARATOR);
	}

	@Before("cflowbelowPointcut()")
	public void beforeCflowbelowPointcut(JoinPoint joinPoint) {

		if (!log.isTraceEnabled()) {
			return;
		}

		try {
			indent.append(' ');
			String taskInfoId = indent.toString() + joinPoint.getSignature().toString();
			TaskInfo taskInfo = requestReportStopWatch.getTaskInfoMap().get(taskInfoId);

			if (taskInfo == null || taskInfo.getStartCount() < LOG_LIMIT_COUNT) {
				logBuilder
						.append(indent)
						.append(joinPoint.getSignature().toString())
						.append(" : start time : ")
						.append(requestReportStopWatch.getTime());
				argumentLogging(joinPoint);
				logBuilder.append(LINE_SEPARATOR);
			} else if (taskInfo.getStartCount() < LOG_LIMIT_COUNT + 1) {
				logBuilder.append(indent).append("...\n");
			}

			requestReportStopWatch.startTask(taskInfoId);
		} catch (Throwable e) {
			log.trace("", e);
		}
	}

	@AfterThrowing(pointcut = "cflowbelowPointcut()", throwing = "t")
	public void afterThrowingCflowbelowPointcut(Throwable t, JoinPoint joinPoint) {

		if (!log.isTraceEnabled()) {
			return;
		}
		try {
			String taskInfoId = indent.toString() + joinPoint.getSignature().toString();
			TaskInfo taskInfo = requestReportStopWatch.getTaskInfoMap().get(taskInfoId);
			if (taskInfo != null && taskInfo.getStartCount() < LOG_LIMIT_COUNT + 1) {
				afterThrowing(t, joinPoint);
			}
		} catch (Throwable e) {
			log.trace("", e);
		}
	}

	@AfterReturning(pointcut = "cflowbelowPointcut()", returning = "o")
	public void afterReturningCflowbelowPointcut(Object o, JoinPoint joinPoint) {

		if (!log.isTraceEnabled()) {
			return;
		}

		try {
			String taskInfoId = indent.toString() + joinPoint.getSignature().toString();
			TaskInfo taskInfo = requestReportStopWatch.getTaskInfoMap().get(taskInfoId);
			if (taskInfo != null && taskInfo.getStartCount() < LOG_LIMIT_COUNT + 1) {
				afterReturning(o, joinPoint);
			}
		} catch (Throwable e) {
			log.trace("", e);
		}
	}

	@After("cflowbelowPointcut()")
	public void afterCflowbelowPointcut(JoinPoint joinPoint) {

		if (!log.isTraceEnabled()) {
			return;
		}

		try {
			String taskInfoId = indent.toString() + joinPoint.getSignature().toString();
			TaskInfo taskInfo = requestReportStopWatch.getTaskInfoMap().get(taskInfoId);

			if (taskInfo != null && taskInfo.getStartCount() < LOG_LIMIT_COUNT + 1) {
				logBuilder
						.append(taskInfoId)
						.append(" : end duration : ")
						.append(requestReportStopWatch.getTime())
						.append(LINE_SEPARATOR);
			}

			indent.delete(indent.length() - 1, indent.length());
			requestReportStopWatch.stopTask(taskInfo);
		} catch (Throwable e) {
			log.trace("", e);
		}
	}

	@After("controllerPointcut()")
	public void afterControllerPointcut(JoinPoint joinPoint) {

		if (!log.isTraceEnabled()) {
			return;
		}

		try {
			String taskInfoId = indent.toString() + joinPoint.getSignature().toString();
			TaskInfo taskInfo = requestReportStopWatch.getTaskInfoMap().get(taskInfoId);

			if (taskInfo != null && taskInfo.getStartCount() < LOG_LIMIT_COUNT + 1) {
				logBuilder
						.append(taskInfoId)
						.append(" : end duration : ")
						.append(requestReportStopWatch.getTime())
						.append(LINE_SEPARATOR);
			}

			requestReportStopWatch.stopTask(taskInfo);
		} catch (Throwable e) {
			log.trace("", e);
		}
	}

	@After("doFilter()")
	public void afterDoFilter(JoinPoint joinPoint) {

		if (!log.isTraceEnabled()) {
			return;
		}

		try {
			requestReportStopWatch.stop();

			logBuilder.append(requestReportStopWatch.print());
			logBuilder.append(LINE_SEPARATOR).append(BANNER_END);

			if (requestMappingLogger != null && requestMappingLogger.isTraceEnabled()) {
				requestMappingLogger.trace(logBuilder.toString());
			}
		} catch (Throwable e) {
			log.trace("", e);
		}
	}

	private void argumentLogging(JoinPoint joinPoint) {
		Object[] args = joinPoint.getArgs();
		if ((args != null) && (args.length > 0)) {
			Signature signature = joinPoint.getSignature();

			if (signature instanceof MethodSignature) {
				MethodSignature ms = (MethodSignature) signature;
				String[] parameterNames = ms.getParameterNames();
				logBuilder.append(" arguments (");

				for (int i = 0; i < args.length; i++) {
					Object arg = args[i];

					argumentLogging(parameterNames[i], arg);

					if (i + 1 < args.length) {
						logBuilder.append(", ");
					}
				}
				logBuilder.append(")");
			}
		}
	}

	private void argumentLogging(String parameterName, Object arg) {
		if (arg instanceof Map) {
			Map<?, ?> map = (Map<?, ?>) arg;
			Set<?> set = map.keySet();
			logBuilder.append(parameterName).append("(").append(set.size()).append(")");
		} else if (arg instanceof Collection) {
			Collection<?> collection = (Collection<?>) arg;
			logBuilder.append(parameterName).append("(").append(collection.size()).append(")");
		} else {
			logBuilder.append(arg);
		}
	}

	private void afterThrowing(Throwable t, JoinPoint joinPoint) {
		try {
			logBuilder
					.append(indent)
					.append(joinPoint.getSignature().toString())
					.append(" : throw ")
					.append(t.getClass().getName())
					.append(LINE_SEPARATOR);
		} catch (Throwable e) {
			log.trace("", e);
		}
	}

	private void afterReturning(Object o, JoinPoint joinPoint) {
		try {
			Signature signature = joinPoint.getSignature();

			if (signature instanceof MethodSignature) {
				MethodSignature ms = (MethodSignature) signature;

				Class<?> returnType = ms.getReturnType();

				if (!void.class.equals(returnType)) {
					logBuilder.append(indent).append(joinPoint.getSignature().toString()).append(" returning ");

					if (o instanceof ModelAndView) {
						modelAndViewLogging((ModelAndView) o);
					} else if (o instanceof Collection) {
						Collection<?> collection = (Collection<?>) o;
						logBuilder.append(returnType.getName()).append(" (").append(collection.size()).append(")");
					} else if (o instanceof Map) {
						Map<?, ?> map = (Map<?, ?>) o;
						Set<?> set = map.keySet();
						logBuilder
								.append(returnType.getName())
								.append(" (")
								.append(set.size())
								.append(") : ")
								.append(set);
					} else if (o instanceof Iterable) {
						logBuilder.append(returnType.getName()).append(" Iterable");
					} else {
						logBuilder.append(returnType.getName()).append(" = ").append(o);
					}

					logBuilder.append(LINE_SEPARATOR);
				}
			}
		} catch (Throwable e) {
			log.trace("", e);
		}
	}

	private void modelAndViewLogging(ModelAndView modelAndView) {
		Map<String, Object> model = modelAndView.getModel();

		if (modelAndView.isReference()) {
			logBuilder.append("reference to view with name '").append(modelAndView.getView()).append("'");
		} else {
			logBuilder.append("materialized View is [").append(modelAndView.getView()).append(']');
		}

		logBuilder.append("; model is ").append("\n");

		modelLogging(logBuilder, model);
	}

	private void modelLogging(StringBuilder buf, Map<String, Object> model) {
		if (model != null) {
			for (Iterator<String> it = model.keySet().iterator(); it.hasNext();) {
				Object key = it.next();
				Object o = model.get(key);

				if (o instanceof java.util.Collection) {
					Collection<?> collection = (Collection<?>) o;
					buf.append(key).append("(").append(collection.size()).append(")").append(",\n");
				} else if (o instanceof java.util.Map) {
					Map<?, ?> map = (Map<?, ?>) o;
					buf.append(key).append("(").append(map.size()).append(")").append(",\n");
				} else {
					buf.append(key).append(",\n");
				}
			}
		}
	}
}
