package tech.easyflow.log.reporter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.io.IOException;

import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;

/**
 * 缓存需要记录的请求和响应正文，并绕过流式或敏感路径。
 */
@Component
@Order(HIGHEST_PRECEDENCE)
@ConditionalOnProperty(
        prefix = "easyflow.log.reporter",
        name = "enabled",
        havingValue = "true",
        matchIfMissing = true  // 默认开启
)
public class ResponseCachingFilter implements Filter {

    private final ActionLogReporterProperties logProperties;

    /**
     * 创建响应缓存过滤器。
     *
     * @param logProperties 日志采集路径配置
     */
    public ResponseCachingFilter(ActionLogReporterProperties logProperties) {
        this.logProperties = logProperties;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String uri = httpRequest.getRequestURI();
        String method = httpRequest.getMethod();

        // OPTIONS 请求通常是预检，不需要缓存正文。
        if ("OPTIONS".equalsIgnoreCase(method)) {
            chain.doFilter(request, response);
            return;
        }

        // 流式下载和敏感媒体路径必须在包装响应前排除，避免截断异步响应或缓存文件正文。
        if (isExcluded(uri)) {
            chain.doFilter(request, response);
            return;
        }

        // 检查是否匹配包含路径（一般为 /**）。
        if (!isIncluded(uri)) {
            chain.doFilter(request, response);
            return;
        }

        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpRequest);
        if (isSseRequest(httpRequest)) {
            // SSE 响应不能经过响应缓存，否则会破坏实时输出。
            chain.doFilter(requestWrapper, response);
            return;
        }

        HttpServletResponse httpResponse = (HttpServletResponse) response;
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(httpResponse);
        try {
            chain.doFilter(requestWrapper, responseWrapper);
        } finally {
            responseWrapper.copyBodyToResponse();
        }
    }

    /**
     * 判断请求路径是否禁止缓存。
     *
     * @param uri 请求路径
     * @return 是否排除
     */
    private boolean isExcluded(String uri) {
        return logProperties.getExcludePatterns().stream().anyMatch(p -> match(uri, p));
    }

    /**
     * 判断请求路径是否需要采集。
     *
     * @param uri 请求路径
     * @return 是否包含
     */
    private boolean isIncluded(String uri) {
        return logProperties.getIncludePatterns().stream().anyMatch(p -> match(uri, p));
    }

    /**
     * 根据标准 Accept 请求头判断是否为 SSE 请求。
     *
     * @param request HTTP 请求
     * @return 是否为 SSE 请求
     */
    private boolean isSseRequest(HttpServletRequest request) {
        String accept = request.getHeader("Accept");
        return accept != null && accept.contains("text/event-stream");
    }

    /**
     * 匹配支持星号和双星号的简单路径模式。
     *
     * @param path 请求路径
     * @param pattern 路径模式
     * @return 是否匹配
     */
    private boolean match(String path, String pattern) {
        if (pattern.equals("/**")) {
            return true;
        }
        if (pattern.endsWith("/**")) {
            String prefix = pattern.substring(0, pattern.length() - 3);
            return path.startsWith(prefix);
        }
        if (pattern.endsWith("*")) {
            String prefix = pattern.substring(0, pattern.length() - 1);
            return path.startsWith(prefix);
        }
        if (pattern.contains("*") && !pattern.contains("/**")) {
            // 支持 *.js, *.css
            String p = pattern.replace("*", "").replace(".", "\\.");
            return path.matches(".*" + p + ".*");
        }
        return path.equals(pattern);
    }
}
