package ru.nbch.credit_tracker.config.rest.filters;

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.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

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

    private static final Set<String> SENSITIVE_HEADERS = Set.of("authorization", "cookie", "set-cookie");
    private static final int MAX_BODY = 10 * 1024;

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        String path = request.getRequestURI();
        return path.startsWith("/actuator/health") || path.startsWith("/swagger")
            || path.startsWith("/v3/api-docs") || path.startsWith("/static/");
    }

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

        long startNs = System.nanoTime();

        ContentCachingRequestWrapper req = wrapRequest(request);
        ContentCachingResponseWrapper resp = new ContentCachingResponseWrapper(response);

        String correlationId = Optional.ofNullable(request.getHeader("X-Correlation-Id"))
            .filter(StringUtils::hasText)
            .orElse(UUID.randomUUID().toString());
        resp.setHeader("X-Correlation-Id", correlationId);

        try {
            chain.doFilter(req, resp);
        } finally {
            long tookMs = (System.nanoTime() - startNs) / 1_000_000;

            String requestBody = readBody(req.getContentAsByteArray(), req.getCharacterEncoding());
            String responseBody = readBody(resp.getContentAsByteArray(), resp.getCharacterEncoding());

            Map<String, String> reqHeaders = headersMap(req);
            Map<String, String> respHeaders = headersMap(resp);

            mask(reqHeaders);
            mask(respHeaders);

            log.info(
                "HTTP {} {}{} | status={} | took={}ms | cid={} | reqHeaders={} | reqBody={} | respHeaders={} | respBody={}",
                req.getMethod(),
                req.getRequestURI(),
                req.getQueryString() == null ? "" : "?" + req.getQueryString(),
                resp.getStatus(),
                tookMs,
                correlationId,
                reqHeaders,
                requestBody,
                respHeaders,
                responseBody
            );

            resp.copyBodyToResponse();
        }
    }

    private static ContentCachingRequestWrapper wrapRequest(HttpServletRequest request) {
        return (request instanceof ContentCachingRequestWrapper c) ? c : new ContentCachingRequestWrapper(request, MAX_BODY);
    }

    private static String readBody(byte[] buf, String encoding) {
        if (buf == null || buf.length == 0) return "";
        var bytes = buf.length > MAX_BODY ? Arrays.copyOf(buf, MAX_BODY) : buf;
        String body = new String(bytes, encoding != null ? java.nio.charset.Charset.forName(encoding) : StandardCharsets.UTF_8);
        if (buf.length > MAX_BODY) body += "…[truncated]";
        return body;
    }

    private static Map<String, String> headersMap(HttpServletRequest req) {
        return Collections.list(req.getHeaderNames()).stream()
            .collect(Collectors.toMap(h -> h, req::getHeader, (a, b) -> a, LinkedHashMap::new));
    }

    private static Map<String, String> headersMap(HttpServletResponse resp) {
        return resp.getHeaderNames().stream()
            .collect(Collectors.toMap(h -> h, resp::getHeader, (a, b) -> a, LinkedHashMap::new));
    }

    private static void mask(Map<String, String> headers) {
        headers.replaceAll((k, v) -> SENSITIVE_HEADERS.contains(k.toLowerCase()) ? "***" : v);
    }
}
