/* (C) 2021 YiRing, Inc. */
package com.yiring.common.aspect;

import cn.hutool.core.io.IoUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Result;
import com.yiring.common.util.IpUtil;
import com.yiring.common.utils.Contexts;
import jakarta.servlet.http.HttpServletRequest;
import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 请求接口切面，记录接口耗时相关
 *
 * @author ifzm
 * @version 0.1
 */

@Slf4j
@Aspect
@Component
public class RequestAspect {

    @Value("${debug}")
    Boolean debug;

    /**
     * 白名单(忽略)
     */
    static final List<String> IGNORE_LIST = List.of("/swagger-resources", "/error", "/v3/api-docs");

    @Pointcut(
        "@annotation(org.springframework.web.bind.annotation.RequestMapping) || @annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.GetMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping) || @annotation(org.springframework.web.bind.annotation.DeleteMapping) || @annotation(org.springframework.web.bind.annotation.PatchMapping) || @annotation(org.springframework.web.bind.annotation.ExceptionHandler)"
    )
    public void apiPointCut() {}

    @Around("apiPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        HttpServletRequest request = Contexts.getRequest();
        String contentType = request.getHeader("Content-Type");

        // 放行白名单
        for (String path : IGNORE_LIST) {
            if (request.getServletPath().startsWith(path)) {
                return point.proceed();
            }
        }

        // 计算接口执行耗时
        long start = System.currentTimeMillis();
        Object result = point.proceed();
        long end = System.currentTimeMillis();

        // 计算接口处理消耗时间，格式化
        String timestamp = LocalDateTime.now().format(DateFormatter.DATE_TIME);
        String times = String.format("%.3fs", (double) (end - start) / 1000);

        // 获取接口请求扩展信息，Header, Params
        String extra = "";
        if (Boolean.TRUE.equals(debug) || log.isDebugEnabled()) {
            String headers = JSONObject.toJSONString(
                JakartaServletUtil.getHeaderMap(request),
                JSONWriter.Feature.PrettyFormat
            );
            extra += String.format("\nHeaders: %s", headers);

            String params = JSONObject.toJSONString(
                JakartaServletUtil.getParamMap(request),
                JSONWriter.Feature.PrettyFormat
            );
            extra += String.format("\nParams: %s", params);

            if (contentType.contains("application/json")) {
                String body = IoUtil.read(request.getInputStream(), Charset.forName(request.getCharacterEncoding()));
                String string = JSONObject.toJSONString(JSONObject.parseObject(body), JSONWriter.Feature.PrettyFormat);
                extra += String.format("\nBody: %s", string);
            }

            if (result instanceof Result) {
                extra +=
                    String.format(
                        "\nResponse: %s, %s",
                        ((Result<?>) result).getStatus(),
                        ((Result<?>) result).getMessage()
                    );
            }
        }

        // 获取接口处理返回的状态码，设置接口响应时间和耗时信息
        int status = 200;
        if (result instanceof Result) {
            ((Result<?>) result).setTimestamp(timestamp);
            ((Result<?>) result).setTimes(times);
            status = ((Result<?>) result).getStatus();
        }

        // 打印请求日志 (Optional Replace: MDC, Trace)
        log.info(
            "[Request] IP: {}, Method: {}, URL: {}, Status: {}, Times: {}{}",
            IpUtil.getClientIp(request),
            request.getMethod(),
            request.getRequestURL(),
            status,
            times,
            extra
        );

        return result;
    }
}
