package ru.ibs.socrates.confexporter.config;

import static java.util.Objects.nonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.stream.Collectors.toList;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;


/**
 * Фильтр для логирования запросов 
 *
 */
@Slf4j
@Component
@ConditionalOnExpression("${logging.filter-logging.enabled:true}")
@Order(-100)
public class LoggingFilterHandler extends OncePerRequestFilter {

  /**
   * Для запроса - размер кеша
   *     сontentCacheLimit the maximum number of bytes to cache per request
   * 
   * Для ответа, если превышение размера, выводит '...'
   */
  @Value("${logging.filter-logging.content-max-size:2000}")
  private Integer contentMaxSize;

  /**
   * Перечень не логируемых эндпоинтов
   */
  private final List<String> disableForEndpointsList;

  /**
   * Сравнение URL
   */
  private final PathMatcher pathMatcher = new AntPathMatcher();

  /**
   * Если contentType содержит один из типов, выводит '...'
   */
  @Value("${logging.filter-logging.content-types-list:octet-stream, form-data}")
  private List<String> disableForContentTypeList;

  public LoggingFilterHandler(
      @Value("${logging.filter.exclude}") String disableForEndpoints,
      @Value("${server.servlet.context-path}") String contextPath) {
    disableForEndpointsList = List.of(disableForEndpoints.split(","))
        .stream()
        .map(value -> contextPath + value)
        .collect(toList());
  }

  /**
   * Вызов APi методов
   */
  final Pattern apiV1Pattern = Pattern.compile(".*?/v\\d+/.+");

  /**
   * Фильтр, для логирования использует ContentCachingRequestWrapper/ContentCachingResponseWrapper
   */
  @Override
  public void doFilterInternal(HttpServletRequest servletRequest,
      HttpServletResponse servletResponse, FilterChain filterChain)
      throws ServletException, IOException {

    final String requestUrl = servletRequest.getRequestURI();
    LoggingMode loggingMode = getLoggingMode(requestUrl);
    boolean isNeedLogging = log.isInfoEnabled() && loggingMode != LoggingMode.NONE;

    if (!isNeedLogging) {
      filterChain.doFilter(servletRequest, servletResponse);
    } else {
      StopWatch stopwatch = new StopWatch();
      stopwatch.start();
      log.info("request started url={}", requestUrl);

      if (loggingMode == LoggingMode.FULL) {
        ContentCachingRequestWrapper requestWrapper =
            new ContentCachingRequestWrapper(servletRequest, contentMaxSize);
        ContentCachingResponseWrapper responseWrapper =
            new ContentCachingResponseWrapper(servletResponse);
        try {
          filterChain.doFilter(requestWrapper, responseWrapper);
        } finally {
          logResponse(requestWrapper, responseWrapper, requestUrl, stopwatch);
        }
      } else {
        try {
          filterChain.doFilter(servletRequest, servletResponse);
        } finally {
          logResponse(servletResponse, requestUrl, stopwatch);
        }
      }
    }
  }

  private void logResponse(ContentCachingRequestWrapper requestWrapper,
      ContentCachingResponseWrapper responseWrapper, String requestUrl,
      StopWatch stopwatch) throws IOException {

    try {
      log.info("request finished url={}, status={}, time={}", requestUrl,
          responseWrapper.getStatus(), stopwatch.getTime(MILLISECONDS));

      if (log.isInfoEnabled()) {
        String requestString;
        var disableRequestLogging = nonNull(requestWrapper.getContentType())
            && disableForContentTypeList.stream()
                .anyMatch(type -> requestWrapper.getContentType().contains(type));
        if (!disableRequestLogging) {
          byte[] requestByteArray = requestWrapper.getContentAsByteArray();
          requestString = new String(requestByteArray, StandardCharsets.UTF_8);
        } else {
          requestString = "...";
        }

        String responseString;
        var disableResponseLogging = nonNull(responseWrapper.getContentType())
            && disableForContentTypeList.stream()
                .anyMatch(type -> responseWrapper.getContentType().contains(type));
        if (responseWrapper.getContentSize() < contentMaxSize && !disableResponseLogging) {
          byte[] responseByteArray = responseWrapper.getContentAsByteArray();
          responseString = new String(responseByteArray, StandardCharsets.UTF_8);
        } else {
          responseString = "...";
        }
        log.info("request finished url={}, request body={}, response body={}", requestUrl,
            requestString, responseString);
      }
    } catch (Exception e) {
      log.error("error", e.getMessage(), e);
    } finally {
      stopwatch.stop();
      /* It is important to copy cached response body back to response stream to see response */
      responseWrapper.copyBodyToResponse();
    }
  }

  private void logResponse(HttpServletResponse servletResponse, String requestUrl,
      StopWatch stopwatch) throws IOException {
    log.info("request finished url={}, status={}, time={}", requestUrl,
        servletResponse.getStatus(), stopwatch.getTime(MILLISECONDS));
    stopwatch.stop();
  }


  private LoggingMode getLoggingMode(String requestUrl) {
    boolean isApi = apiV1Pattern.matcher(requestUrl).matches();
    if (isApi) {
      boolean isSwagger = disableForEndpointsList.stream()
          .anyMatch(endpoint -> pathMatcher.match(endpoint, requestUrl));
      if (isSwagger) {
        return LoggingMode.NONE;
      } else {
        return LoggingMode.FULL;
      }
    } else {
      return LoggingMode.NONE;
    }
  }

  private enum LoggingMode {
    FULL, PARTIAL, NONE;
  }
}

