package com.bio.bio_backend.global.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.core.annotation.Order;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import org.slf4j.MDC;

@Slf4j
@Component
@Order(2)
public class HttpLoggingFilter extends OncePerRequestFilter {

    private static final int MAX_LOG_BODY = 10 * 1024; // 10 KB

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        long startTime = System.currentTimeMillis();

        ContentCachingRequestWrapper wrappedRequest =
                (request instanceof ContentCachingRequestWrapper)
                        ? (ContentCachingRequestWrapper) request
                        : new ContentCachingRequestWrapper(request);

        ContentCachingResponseWrapper wrappedResponse =
                (response instanceof ContentCachingResponseWrapper)
                        ? (ContentCachingResponseWrapper) response
                        : new ContentCachingResponseWrapper(response);

        log.info("********************************************************************************");
        log.info("* [START] HTTP LOGGING | TRACE ID: {}", getCurrentTraceId());
        log.info("* Method: {} | URI: {}", wrappedRequest.getMethod(), wrappedRequest.getRequestURI());
        log.info("* Headers: {}", getRequestHeaders(wrappedRequest));
        log.info("********************************************************************************");

        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            String reqBody = extractRequestBody(wrappedRequest);
            String resBody = extractResponseBody(wrappedResponse);

            log.info("********************************************************************************");
            log.info("* [END] HTTP LOGGING");
            log.info("* Method: {} | URI: {}", wrappedRequest.getMethod(), wrappedRequest.getRequestURI());
            log.info("* Status: {} | Duration: {}ms", wrappedResponse.getStatus(), duration);
            log.info("* Req Body: {}", maskSensitiveData(reqBody));
            log.info("* Res Headers: {}", getResponseHeaders(wrappedResponse));
            log.info("* Res Body: {}", maskSensitiveData(resBody));
            log.info("********************************************************************************");

            wrappedResponse.copyBodyToResponse();
        }
    }

    private String extractRequestBody(ContentCachingRequestWrapper wrapper) {
        // multipart/form-data 등은 보통 로깅 제외
        String ct = Optional.ofNullable(wrapper.getContentType()).orElse("");
        if (ct.toLowerCase(Locale.ROOT).startsWith("multipart/")) {
            return "(multipart skipped)";
        }

        byte[] buf = wrapper.getContentAsByteArray();
        if (buf == null || buf.length == 0) return "";
        Charset cs = resolveCharset(wrapper.getCharacterEncoding());
        String s = new String(buf, cs);
        return truncate(s, MAX_LOG_BODY);
    }

    private String extractResponseBody(ContentCachingResponseWrapper wrapper) {
        byte[] buf = wrapper.getContentAsByteArray();
        if (buf == null || buf.length == 0) return "";
        
        // UTF-8로 고정하여 한글 깨짐 방지
        String s = new String(buf, StandardCharsets.UTF_8);
        return truncate(s, MAX_LOG_BODY);
    }

    private Charset resolveCharset(String enc) {
        try {
            return (enc != null) ? Charset.forName(enc) : StandardCharsets.UTF_8;
        } catch (Exception e) {
            return StandardCharsets.UTF_8;
        }
    }

    private String truncate(String s, int max) {
        if (s == null) return "N/A";
        if (s.length() <= max) return s;
        return s.substring(0, max) + "...(truncated)";
    }

    // 민감 정보 마스킹 (간단/범용)
    private String maskSensitiveData(String body) {
        if (body == null || body.isEmpty()) return "N/A";
        // JSON 키 기준 토큰/비번류 마스킹 (대소문자 무시)
        String masked = body
                .replaceAll("(?i)(\"password\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3")
                .replaceAll("(?i)(\"AccessToken\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3")
                .replaceAll("(?i)(\"RefreshToken\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3")
                .replaceAll("(?i)(\"authorization\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3");
        // x-www-form-urlencoded 형태의 password 파라미터 마스킹
        masked = masked.replaceAll("(?i)(^|[&])password=([^&]*)", "$1password=***");
        return masked;
    }

    private String getRequestHeaders(HttpServletRequest request) {
        Map<String, String> headers = new LinkedHashMap<>();
        // 필요한 헤더만 추리거나, 전부 찍고 민감한 건 마스킹
        Collections.list(request.getHeaderNames()).forEach(name -> {
            String value = request.getHeader(name);
            if (value == null) {
                headers.put(name, null);
                return;
            }

            if ("cookie".equalsIgnoreCase(name)) {
                headers.put(name, maskCookieHeader(value));
                return;
            }

            // 기타 헤더는 그대로 기록 (요구사항에 따라 최소 마스킹 적용)
            headers.put(name, value);
        });
        return headers.toString();
    }

    private String getResponseHeaders(HttpServletResponse response) {
        Map<String, String> headers = new LinkedHashMap<>();
        for (String name : response.getHeaderNames()) {
            if ("set-cookie".equalsIgnoreCase(name)) {
                List<String> values = new ArrayList<>(response.getHeaders(name));
                List<String> masked = new ArrayList<>();
                for (String v : values) {
                    masked.add(maskSetCookieHeader(v));
                }
                headers.put(name, String.join(",", masked));
                continue;
            }

            headers.put(name, String.join(",", response.getHeaders(name)));
        }
        if (response.getContentType() != null) {
            headers.putIfAbsent("Content-Type", response.getContentType());
        }
        return headers.toString();
    }

    // Cookie 헤더: "a=1; AccessToken=xxx; RefreshToken=yyy" 형태 중 AccessToken/RefreshToken만 마스킹
    private String maskCookieHeader(String cookieHeader) {
        if (cookieHeader == null || cookieHeader.isEmpty()) return cookieHeader;
        String masked = cookieHeader
                .replaceAll("(?i)(^|;\\s*)(AccessToken)=([^;]*)", "$1$2=***")
                .replaceAll("(?i)(^|;\\s*)(RefreshToken)=([^;]*)", "$1$2=***");
        return masked;
    }

    // Set-Cookie: "AccessToken=xxx; Path=/; HttpOnly; ..." 형태 중 AccessToken/RefreshToken만 마스킹
    private String maskSetCookieHeader(String setCookie) {
        if (setCookie == null || setCookie.isEmpty()) return setCookie;
        String masked = setCookie
                .replaceAll("(?i)^(AccessToken)=([^;]*)", "$1=***")
                .replaceAll("(?i)^(RefreshToken)=([^;]*)", "$1=***");
        return masked;
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        String path = request.getRequestURI();
        return path.startsWith("/static/")
                || path.startsWith("/css/")
                || path.startsWith("/js/")
                || path.startsWith("/images/")
                || path.equals("/favicon.ico")
                || path.startsWith("/actuator/health");
    }

    /**
     * 현재 스레드의 TRACE ID를 반환합니다.
     * @return TRACE ID 또는 "N/A"
     */
    private String getCurrentTraceId() {
        String traceId = MDC.get("traceId");
        return traceId != null ? traceId : "N/A";
    }
}
