package tech.easyflow.log;

import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.fastjson2.JSON;
import jakarta.servlet.http.HttpServletRequest;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.util.RequestUtil;
import tech.easyflow.common.util.StringUtil;
import tech.easyflow.log.annotation.LogRecord;
import tech.easyflow.log.entity.WriteLog;
import tech.easyflow.log.mapper.WriteLogMapper;

import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.Date;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
 * 记录管理端 Controller 操作结果及最小必要请求摘要。
 */
@Aspect
@Component
public class LogAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(LogAspect.class);
    private static final int MAX_PARAMETER_VALUE_LENGTH = 512;
    private static final int MAX_IDENTIFIER_LENGTH = 128;
    private static final int STATUS_SUCCESS = 1;
    private static final int STATUS_FAILED = 9;
    private static final String SKILL_API_PREFIX = "/api/v1/skill";
    private static final List<String> SKILL_IDENTIFIER_KEYS =
            List.of("id", "resourceId", "skillId", "sourceId");
    private static final Set<String> READ_ONLY_HTTP_METHODS =
            Set.of("GET", "HEAD", "OPTIONS");

    private final WriteLogMapper logService;
    private final LogRecordProperties config;

    /**
     * 创建操作日志切面。
     *
     * @param logService 操作日志写入 Mapper
     * @param config 操作日志配置
     */
    public LogAspect(WriteLogMapper logService, LogRecordProperties config) {
        this.logService = logService;
        this.config = config;
    }

    /**
     * 匹配管理端控制器与通用增删改查控制器方法。
     */
    @Pointcut("within(@org.springframework.web.bind.annotation.RestController *) " +
            "|| execution(* tech.easyflow.common.web.controller.BaseCurdController.*(..))")
    public void pointcut() {
    }

    /**
     * 执行业务方法并按配置持久化操作日志。
     *
     * @param proceedingJoinPoint 当前连接点
     * @return 业务方法返回值
     * @throws Throwable 业务方法或日志写入异常
     */
    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        String servletPath = request.getServletPath();

        //匹配前缀
        if (StringUtil.hasText(config.getRecordActionPrefix()) && !servletPath.startsWith(config.getRecordActionPrefix())) {
            return proceedingJoinPoint.proceed();
        }

        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Class<?> controllerClass = signature.getDeclaringType();
        Method method = signature.getMethod();
        LogRecord logRecord = method.getAnnotation(LogRecord.class);
        if (!shouldRecordRequest(request, logRecord)) {
            return proceedingJoinPoint.proceed();
        }
        String params = getRequestParamsString(request);
        Throwable actionFailure = null;

        try {
            return proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            actionFailure = throwable;
            throw throwable;
        } finally {
            WriteLog sysLog = new WriteLog();
            if (StpUtil.isLogin()) {

                BigInteger accountId = SaTokenUtil.getLoginAccount().getId();
                sysLog.setAccountId(accountId);
            }
            sysLog.setActionName(buildActionName(logRecord, method));
            sysLog.setActionType(logRecord != null ? logRecord.actionType() : null);
            sysLog.setActionClass(controllerClass.getName());
            sysLog.setActionMethod(method.getName());
            sysLog.setActionUrl(request.getRequestURL().toString());
            sysLog.setActionIp(RequestUtil.getIpAddress(request));
            sysLog.setActionParams(params);
            sysLog.setActionBody(getSkillAuditBody(request));
            sysLog.setStatus(actionFailure == null ? STATUS_SUCCESS : STATUS_FAILED);
            sysLog.setCreated(new Date());

            persistLog(sysLog, actionFailure);
        }
    }

    /**
     * 判断当前请求是否需要写入操作日志。
     *
     * @param request 当前 HTTP 请求
     * @param logRecord 方法上的显式日志标记
     * @return 需要记录返回 {@code true}
     */
    private boolean shouldRecordRequest(HttpServletRequest request, LogRecord logRecord) {
        if (config.isRecordReadActions() || logRecord != null) {
            return true;
        }
        String httpMethod = request.getMethod();
        return httpMethod == null
                || !READ_ONLY_HTTP_METHODS.contains(httpMethod.toUpperCase(Locale.ROOT));
    }

    /**
     * 持久化操作日志；业务动作已失败时，日志异常不得覆盖原始异常。
     *
     * @param sysLog 操作日志
     * @param actionFailure 业务动作异常，无异常时为 {@code null}
     */
    private void persistLog(WriteLog sysLog, Throwable actionFailure) {
        try {
            logService.insert(sysLog);
        } catch (RuntimeException logFailure) {
            if (actionFailure == null) {
                throw logFailure;
            }
            if (actionFailure != logFailure) {
                actionFailure.addSuppressed(logFailure);
            }
            LOGGER.error("记录失败操作日志时发生异常，原始业务异常将继续向上传递", logFailure);
        }
    }

    /**
     * 解析日志动作名称。
     *
     * @param logRecord 显式日志标记
     * @param method 当前控制器方法
     * @return 日志动作名称
     */
    private String buildActionName(LogRecord logRecord, Method method) {
        if (logRecord != null && StringUtil.hasText(logRecord.value())) {
            return logRecord.value();
        } else {
            //todo 这里可以通过方法名，去获取 Controller 的实体类，在获取其表备注信息，进一步进行判断
            return method.getName();
        }
    }

    /**
     * 提取 Skill 写请求中的顶层资源标识，不记录正文、配置或其他业务字段。
     *
     * @param request 当前 HTTP 请求
     * @return 标识摘要 JSON；非 Skill JSON 请求或无可用标识时返回 {@code null}
     */
    private String getSkillAuditBody(HttpServletRequest request) {
        String servletPath = request.getServletPath();
        if (!StringUtil.hasText(servletPath)
                || !(servletPath.equals(SKILL_API_PREFIX) || servletPath.startsWith(SKILL_API_PREFIX + "/"))
                || !isJsonContentType(request.getContentType())) {
            return null;
        }
        try {
            // JsonBodyArgumentResolver 会把已解析对象缓存在请求属性中，正常路径无需重复读取和解析正文。
            Object parsed = RequestUtil.readJsonObjectOrArray(request);
            if (!(parsed instanceof Map<?, ?> values)) {
                return null;
            }
            Map<String, Object> identifiers = new LinkedHashMap<>();
            for (String key : SKILL_IDENTIFIER_KEYS) {
                Object value = safeIdentifier(values.get(key));
                if (value != null) {
                    identifiers.put(key, value);
                }
            }
            return identifiers.isEmpty() ? null : JSON.toJSONString(identifiers);
        } catch (RuntimeException ignored) {
            return null;
        }
    }

    /**
     * 判断请求内容是否为 JSON。
     *
     * @param contentType Content-Type 请求头
     * @return 是否为 JSON 媒体类型
     */
    private boolean isJsonContentType(String contentType) {
        if (!StringUtil.hasText(contentType)) {
            return false;
        }
        String normalized = contentType.toLowerCase(java.util.Locale.ROOT);
        return normalized.startsWith("application/json") || normalized.contains("+json");
    }

    /**
     * 将资源标识收敛为安全、有界的日志值。
     *
     * @param value 原始标识值
     * @return 可记录标识；类型不受支持或为空时返回 {@code null}
     */
    private Object safeIdentifier(Object value) {
        if (value instanceof Number) {
            return value;
        }
        if (value instanceof CharSequence sequence) {
            String identifier = sequence.toString().trim();
            if (identifier.isEmpty()) {
                return null;
            }
            return identifier.length() <= MAX_IDENTIFIER_LENGTH
                    ? identifier : identifier.substring(0, MAX_IDENTIFIER_LENGTH);
        }
        return null;
    }

    /**
     * 提取并限制请求查询参数，避免超长内容进入数据库。
     *
     * @param request 当前 HTTP 请求
     * @return 有界查询参数文本
     */
    private String getRequestParamsString(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        Enumeration<String> e = request.getParameterNames();
        if (e.hasMoreElements()) {
            while (e.hasMoreElements()) {
                String name = e.nextElement();
                String[] values = request.getParameterValues(name);
                if (values.length == 1) {
                    sb.append(name).append("=");
                    if (values[0] != null && values[0].length() > MAX_PARAMETER_VALUE_LENGTH) {
                        sb.append(values[0], 0, MAX_PARAMETER_VALUE_LENGTH).append("...");
                    } else {
                        sb.append(values[0]);
                    }
                } else {
                    sb.append(name).append("[]={");
                    for (int i = 0; i < values.length; i++) {
                        if (i > 0) {
                            sb.append(",");
                        }
                        sb.append(values[i]);
                    }
                    sb.append("}");
                }
                sb.append("  ");
            }
        }
        return sb.toString();
    }


}
