package com.ema.common.aspect.logger;


import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.ema.common.domain.enums.BooleanEnum;
import com.ema.common.domain.vo.IPVo;
import com.ema.common.tools.ip2region.IP2RegionService;
import com.ema.common.utils.ArgsDesensitizationUtil;
import com.ema.common.utils.NetworkUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Data;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * 请求日志记录切面
 * <p>
 * 配置示例：
 * <pre>
 * logging:
 *   operlog:
 *     enabled: true        # 是否开启操作日志
 *     loggerHandler: com.ema.admin.handler.OperLoggerHandler  # 日志处理器
 *     args: true           # 是否记录请求参数
 *     deseFields: password,pwd,oldPwd,newPwd  # 敏感字段脱敏
 *     logOnErr: false      # 是否仅在异常时记录
 * </pre>
 *
 * @author LIJIAN
 * @since 2026-03-25
 */
@Slf4j
@Data
@Aspect
@Component
public class LoggerAspect {

    @Resource
    private AopLogConfig aopLogConfig;
    @Resource
    private ConfigurableApplicationContext context;
    @Resource
    private IP2RegionService ip2RegionService;

    private static final String DEFAULT_MODULE = "未知模块";
    private static final String DEFAULT_TITLE = "未知操作";
    private static final String REGION_SEPARATOR = "|";
    private static final int MAX_LOG_LENGTH = 2000;

    /**
     * 切点：拦截带有 @Logger 注解的方法
     */
    @Pointcut("@annotation(com.ema.common.aspect.logger.Logger)")
    public void logPointcut() {
    }

    /**
     * 环绕通知：记录方法执行日志
     *
     * @param joinPoint 连接点
     * @param logger    日志注解
     * @return 方法执行结果
     * @throws Throwable 方法执行异常
     */
    @Around("@annotation(logger)")
    public Object around(ProceedingJoinPoint joinPoint, Logger logger) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        String errorMsg = null;
        try {
            stopWatch.start();
            return joinPoint.proceed();
        } catch (Throwable e) {
            errorMsg = truncateErrorMessage(e.getMessage());
            throw e; // 重新抛出异常，让上层处理
        } finally {
            stopWatch.stop();
            handleLogger(joinPoint, logger, stopWatch.getTotalTimeMillis(), errorMsg);
        }
    }

    /**
     * 处理日志记录
     *
     * @param point           连接点
     * @param logger          日志注解
     * @param totalTimeMillis 执行耗时（毫秒）
     * @param errorMsg        错误信息
     */
    private void handleLogger(ProceedingJoinPoint point, Logger logger, long totalTimeMillis, String errorMsg) {

        try {
            HttpServletRequest request = NetworkUtil.getHttpServletRequest();
            if (!shouldLog(request, errorMsg)) {
                return;
            }
            // 获取日志处理器并处理日志
            LoggerHandler loggerHandler = context.getBean(aopLogConfig.getLoggerHandler());
            LoggerVo loggerVo = buildLoggerVo(request, point, logger, totalTimeMillis, errorMsg);
            loggerHandler.handler(loggerVo);
        } catch (Exception e) {
            log.error("日志记录失败：{}", e.getMessage(), e);
        }
    }

    /**
     * 构建日志对象
     *
     * @param request         HTTP请求
     * @param point           连接点
     * @param logger          日志注解
     * @param totalTimeMillis 执行耗时
     * @param errorMsg        错误信息
     * @return 日志对象
     */
    private LoggerVo buildLoggerVo(HttpServletRequest request,
                                   ProceedingJoinPoint point,
                                   Logger logger,
                                   long totalTimeMillis,
                                   String errorMsg) {
        LoggerVo loggerVo = new LoggerVo();

        // 设置基础信息
        loggerVo.setType(logger.type().getValue());
        loggerVo.setStatus(StrUtil.isNotBlank(errorMsg) ? BooleanEnum.FALSE.getValue() : BooleanEnum.TRUE.getValue());
        loggerVo.setUrl(request.getRequestURI());
        loggerVo.setOperTime(new Date());
        loggerVo.setDuration(totalTimeMillis);
        loggerVo.setErrorMsg(errorMsg);

        // 设置请求环境信息
        setRequestEnvironmentInfo(request, point, loggerVo);

        // 设置方法信息
        setMethodInfo(request, point, logger, loggerVo);

        return loggerVo;
    }

    /**
     * 设置请求环境信息（IP、地区、浏览器、操作系统）
     *
     * @param request  HTTP请求
     * @param loggerVo 日志对象
     */
    private void setRequestEnvironmentInfo(HttpServletRequest request, ProceedingJoinPoint point, LoggerVo loggerVo) {
        loggerVo.setBrowser(NetworkUtil.getBrowser(request));
        loggerVo.setOs(NetworkUtil.getOs(request));
        loggerVo.setIp(NetworkUtil.getClientIp(request));

        // 记录请求参数（脱敏处理）
        if (aopLogConfig.isArgs()) {
            String filteredParams = ArgsDesensitizationUtil.filterArgs(
                    point.getArgs(),
                    aopLogConfig.getDeseFields()
            );
            loggerVo.setParams(filteredParams);
        }

        // 解析IP地理位置
        String ipLocation = parseIPRegion(loggerVo.getIp());
        loggerVo.setRegion(ipLocation);
    }

    /**
     * 解析IP地理位置
     *
     * @param ip IP地址
     * @return 地理位置
     */
    private String parseIPRegion(String ip) {
        if (StrUtil.isBlank(ip)) {
            return null;
        }

        try {
            IPVo ipVo = ip2RegionService.getIPLocation(ip);
            if (ipVo != null) {
                return String.join(REGION_SEPARATOR,
                        ipVo.getCountry(),
                        ipVo.getProvince(),
                        ipVo.getCity(),
                        ipVo.getCompany()
                );
            }
        } catch (Exception e) {
            log.warn("IP地理位置解析失败：{}，错误：{}", ip, e.getMessage());
        }

        return null;
    }

    /**
     * 设置方法信息（模块、标题、方法名）
     *
     * @param request  HTTP请求
     * @param point    连接点
     * @param logger   日志注解
     * @param loggerVo 日志对象
     */
    private void setMethodInfo(HttpServletRequest request,
                               ProceedingJoinPoint point,
                               Logger logger,
                               LoggerVo loggerVo) {
        // 设置模块名称
        loggerVo.setModule(getModuleName(request, point, logger));
        // 设置操作标题
        loggerVo.setTitle(getOperationTitle(point, logger));
        // 设置方法名
        loggerVo.setMethod(setMethodName(point));
    }

    /**
     * 获取模块名称
     *
     * @param request HTTP请求
     * @param point   连接点
     * @param logger  日志注解
     * @return 模块名称
     */
    private String getModuleName(HttpServletRequest request, ProceedingJoinPoint point, Logger logger) {
        // 优先使用注解指定的模块名
        if (StrUtil.isNotBlank(logger.module())) {
            return logger.module();
        }

        // 其次使用@Api注解的tags
        Api api = point.getTarget().getClass().getAnnotation(Api.class);
        if (api != null && ArrayUtil.isNotEmpty(api.tags())) {
            return api.tags()[0];
        }

        // 最后使用类名
        return point.getTarget().getClass().getSimpleName();
    }

    /**
     * 获取操作标题
     *
     * @param point  连接点
     * @param logger 日志注解
     * @return 操作标题
     */
    private String getOperationTitle(ProceedingJoinPoint point, Logger logger) {
        // 优先使用注解指定的标题
        if (StrUtil.isNotBlank(logger.name())) {
            return logger.name();
        }

        // 其次使用@ApiOperation注解的value
        MethodSignature signature = (MethodSignature) point.getSignature();
        if (signature != null) {
            ApiOperation apiOperation = signature.getMethod().getAnnotation(ApiOperation.class);
            if (apiOperation != null && StrUtil.isNotBlank(apiOperation.value())) {
                return apiOperation.value();
            }
        }

        // 最后使用方法名
        return signature != null ? signature.getName() : DEFAULT_TITLE;
    }

    /**
     * 设置方法名
     *
     * @param point
     */
    private String setMethodName(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        return point.getTarget().getClass().getSimpleName() + "." + signature.getName();
    }

    /**
     * 判断是否需要记录日志
     *
     * @param request  HTTP请求
     * @param errorMsg 错误信息
     * @return true-需要记录，false-不需要记录
     */
    private boolean shouldLog(HttpServletRequest request, String errorMsg) {
        // 基础校验
        if (request == null) {
            log.debug("HttpServletRequest为空，跳过日志记录");
            return false;
        }
        if (aopLogConfig == null) {
            log.debug("AopLogConfig未配置，跳过日志记录");
            return false;
        }
        if (!aopLogConfig.isEnabled()) {
            return false;
        }
        if (aopLogConfig.getLoggerHandler() == null) {
            log.warn("日志处理器未配置，跳过日志记录");
            return false;
        }
        // 仅在异常时记录
        if (aopLogConfig.isLogOnErr() && errorMsg == null) {
            return false;
        }
        return true;
    }

    /**
     * 截断过长的错误信息
     *
     * @param errorMsg 原始错误信息
     * @return 截断后的错误信息
     */
    private String truncateErrorMessage(String errorMsg) {
        if (StrUtil.isBlank(errorMsg))
            return null;
        return errorMsg.length() > MAX_LOG_LENGTH ? errorMsg.substring(0, MAX_LOG_LENGTH) + "..." : "";
    }
}
