package cn.iocoder.yudao.module.system.framework.mybatis.interceptor;

import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.module.system.dal.dataobject.logger.DatabaseOperateLogDO;
import cn.iocoder.yudao.module.system.service.logger.DatabaseOperateLogService;
import com.alibaba.fastjson.JSON;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

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

/**
 * 数据库操作日志拦截器
 * 拦截所有 INSERT、UPDATE、DELETE 操作并记录日志
 *
 * 注意：该拦截器放在 system-biz 模块，避免 framework 对 biz 的依赖
 *
 * @author 芋道源码 / 项目二次开发
 */
@Slf4j
@Component
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class DatabaseOperateLogInterceptor implements Interceptor {

    /**
     * 需要排除的表（避免日志表自身产生日志）
     */
    private static final Set<String> EXCLUDE_TABLES = new HashSet<>(Arrays.asList(
            "system_database_operate_log",
            "system_operate_log",
            "system_login_log",
            "system_error_log"
    ));

    /**
     * 懒加载注入，避免在 MyBatis 初始化 sqlSessionFactory 时提前创建 Service/Mapper 导致循环依赖
     */
    @Resource
    @Lazy
    private DatabaseOperateLogService databaseOperateLogService;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result;

        try {
            // 执行原始 SQL
            result = invocation.proceed();
            // 成功日志
            recordOperateLog(invocation, null, System.currentTimeMillis() - startTime, null);
        } catch (Exception e) {
            // 失败日志
            recordOperateLog(invocation, null, System.currentTimeMillis() - startTime, e);
            throw e;
        }
        return result;
    }

    /**
     * 记录操作日志
     */
    private void recordOperateLog(Invocation invocation, Object result, long costTime, Exception exception) {
        try {
            MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
            Object parameter = invocation.getArgs()[1];

            // 获取 SQL 类型
            SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
            // 只记录 INSERT、UPDATE、DELETE 操作
            if (sqlCommandType != SqlCommandType.INSERT
                    && sqlCommandType != SqlCommandType.UPDATE
                    && sqlCommandType != SqlCommandType.DELETE) {
                return;
            }

            // 获取 SQL 和参数
            BoundSql boundSql = mappedStatement.getBoundSql(parameter);
            String sql = boundSql.getSql();
            Object sqlParams = boundSql.getParameterObject();

            // 获取表名（优先从 SQL 中提取，如果失败则从 Mapper ID 提取）
            String tableName = extractTableNameFromSql(sql);
            if (StrUtil.isBlank(tableName)) {
                tableName = getTableName(mappedStatement);
            }
            if (StrUtil.isBlank(tableName)) {
                return;
            }
            String lowerTableName = tableName.toLowerCase();
            // 排除日志表本身，避免对 system_database_operate_log 及其按月分表（system_database_operate_log_yyyyMM）再次记录日志，产生递归
            if (EXCLUDE_TABLES.contains(lowerTableName) || lowerTableName.startsWith("system_database_operate_log")) {
                return;
            }

            // 构建日志对象
            DatabaseOperateLog log = new DatabaseOperateLog();
            log.setOperateType(sqlCommandType.name());
            log.setTableName(tableName);
            log.setSqlStatement(sql);
            log.setSqlParams(JSON.toJSONString(sqlParams));
            log.setCostTime(costTime);
            log.setSuccess(exception == null);
            log.setErrorMessage(exception != null ? exception.getMessage() : null);

            // 获取用户信息
            try {
                Long userId = WebFrameworkUtils.getLoginUserId() == null ? SecurityFrameworkUtils.getLoginUserId() : WebFrameworkUtils.getLoginUserId();
                Integer userType = WebFrameworkUtils.getLoginUserType() == null ?
                        (SecurityFrameworkUtils.getLoginUser() == null ? null : SecurityFrameworkUtils.getLoginUser().getUserType()) :
                        WebFrameworkUtils.getLoginUserType();
                String userNickname = SecurityFrameworkUtils.getLoginUserNickname();

                log.setUserId(userId);
                log.setUserType(userType);
                log.setUserNickname(userNickname);
            } catch (Exception ignore) {
                // 非 Web 环境或未登录，忽略
            }

            // 获取请求信息
            try {
                HttpServletRequest request = WebFrameworkUtils.getRequest();
                if (request != null) {
                    log.setRequestMethod(request.getMethod());
                    log.setRequestUrl(request.getRequestURI());
                    // 获取客户端 IP
                    String ip = request.getHeader("X-Forwarded-For");
                    if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
                        ip = request.getHeader("X-Real-IP");
                    }
                    if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
                        ip = request.getRemoteAddr();
                    }
                    log.setRequestIp(ip);
                }
            } catch (Exception ignore) {
                // 非 Web 环境，忽略
            }

            // 获取主键信息 & 前后数据
            if (parameter != null) {
                extractPrimaryKeyInfo(parameter, log, sqlCommandType);
            }

            // 异步保存日志
            saveLogAsync(log);
        } catch (Exception e) {
            log.error("[recordOperateLog][记录数据库操作日志失败]", e);
        }
    }

    /**
     * 从 Mapper ID 推断表名
     */
    private String getTableName(MappedStatement mappedStatement) {
        String id = mappedStatement.getId();

        // 例如：cn.iocoder.yudao.module.system.dal.mysql.user.UserMapper.insert -> user
        if (id != null && id.contains(".")) {
            String mapperName = id.substring(0, id.lastIndexOf("."));
            if (mapperName.contains(".")) {
                mapperName = mapperName.substring(mapperName.lastIndexOf(".") + 1);
            }
            if (mapperName.endsWith("Mapper")) {
                String entityName = mapperName.substring(0, mapperName.length() - 6);
                return camelToUnderscore(entityName);
            }
        }
        return null;
    }

    /**
     * 从 SQL 文本中提取表名
     */
    private String extractTableNameFromSql(String sql) {
        if (StrUtil.isBlank(sql)) {
            return null;
        }
        sql = sql.toLowerCase().trim();

        // INSERT
        if (sql.startsWith("insert into")) {
            int start = sql.indexOf("insert into") + "insert into".length();
            while (start < sql.length() && Character.isWhitespace(sql.charAt(start))) {
                start++;
            }
            int end = start;
            while (end < sql.length()
                    && !Character.isWhitespace(sql.charAt(end))
                    && sql.charAt(end) != '(') {
                end++;
            }
            if (end > start) {
                return cleanTableName(sql.substring(start, end));
            }
        }

        // UPDATE
        if (sql.startsWith("update")) {
            int start = sql.indexOf("update") + "update".length();
            while (start < sql.length() && Character.isWhitespace(sql.charAt(start))) {
                start++;
            }
            int end = start;
            while (end < sql.length()
                    && !Character.isWhitespace(sql.charAt(end))) {
                end++;
            }
            if (end > start) {
                return cleanTableName(sql.substring(start, end));
            }
        }

        // DELETE
        if (sql.startsWith("delete from")) {
            int start = sql.indexOf("delete from") + "delete from".length();
            while (start < sql.length() && Character.isWhitespace(sql.charAt(start))) {
                start++;
            }
            int end = start;
            while (end < sql.length()
                    && !Character.isWhitespace(sql.charAt(end))) {
                end++;
            }
            if (end > start) {
                return cleanTableName(sql.substring(start, end));
            }
        }

        return null;
    }

    private String cleanTableName(String raw) {
        String table = raw.trim();
        return table.replace("`", "")
                .replace("\"", "")
                .replace("[", "")
                .replace("]", "");
    }

    /**
     * 提取主键信息 & 前后数据
     */
    private void extractPrimaryKeyInfo(Object parameter, DatabaseOperateLog dbLog, SqlCommandType sqlCommandType) {
        try {
            if (parameter instanceof Map) {
                Map<?, ?> paramMap = (Map<?, ?>) parameter;
                for (Object value : paramMap.values()) {
                    if (value instanceof Collection) {
                        Collection<?> collection = (Collection<?>) value;
                        if (!collection.isEmpty()) {
                            extractPrimaryKeyFromObject(collection.iterator().next(), dbLog, sqlCommandType);
                        }
                    } else if (value != null
                            && !(value instanceof String)
                            && !(value instanceof Number)) {
                        extractPrimaryKeyFromObject(value, dbLog, sqlCommandType);
                    }
                }
            } else if (parameter instanceof Collection) {
                Collection<?> collection = (Collection<?>) parameter;
                if (!collection.isEmpty()) {
                    extractPrimaryKeyFromObject(collection.iterator().next(), dbLog, sqlCommandType);
                }
            } else {
                extractPrimaryKeyFromObject(parameter, dbLog, sqlCommandType);
            }
        } catch (Exception e) {
            log.debug("[extractPrimaryKeyInfo][提取主键信息失败]", e);
        }
    }

    private void extractPrimaryKeyFromObject(Object obj, DatabaseOperateLog dbLog, SqlCommandType sqlCommandType) {
        if (obj == null) {
            return;
        }
        try {
            MetaObject metaObject = SystemMetaObject.forObject(obj);

            // 尝试获取 id 字段
            Object idValue = metaObject.getValue("id");
            if (idValue != null) {
                dbLog.setPrimaryKey("id");
                dbLog.setPrimaryKeyValue(String.valueOf(idValue));
            }

            // UPDATE / DELETE：记录操作前数据
            if ((sqlCommandType == SqlCommandType.UPDATE || sqlCommandType == SqlCommandType.DELETE)
                    && obj instanceof BaseDO) {
                dbLog.setBeforeData(JSON.toJSONString(obj));
            }
            // INSERT / UPDATE：记录操作后数据
            if ((sqlCommandType == SqlCommandType.INSERT || sqlCommandType == SqlCommandType.UPDATE)
                    && obj instanceof BaseDO) {
                dbLog.setAfterData(JSON.toJSONString(obj));
            }
        } catch (Exception e) {
            log.debug("[extractPrimaryKeyFromObject][从对象提取主键信息失败]", e);
        }
    }

    /**
     * 异步保存日志（由 Service 上的 @Async 保证异步）
     */
    private void saveLogAsync(DatabaseOperateLog dbLog) {
        try {
            DatabaseOperateLogDO logDO = convertToDO(dbLog);
            if (logDO != null) {
                databaseOperateLogService.createDatabaseOperateLog(logDO);
            }
        } catch (Exception e) {
            log.error("[saveLogAsync][异步保存数据库操作日志失败]", e);
        }
    }

    private DatabaseOperateLogDO convertToDO(DatabaseOperateLog log) {
        if (log == null) {
            return null;
        }
        DatabaseOperateLogDO logDO = new DatabaseOperateLogDO();
        logDO.setOperateType(log.getOperateType());
        logDO.setTableName(log.getTableName());
        logDO.setPrimaryKey(log.getPrimaryKey());
        logDO.setPrimaryKeyValue(log.getPrimaryKeyValue());
        logDO.setBeforeData(log.getBeforeData());
        logDO.setAfterData(log.getAfterData());
        logDO.setSqlStatement(log.getSqlStatement());
        logDO.setSqlParams(log.getSqlParams());
        logDO.setUserId(log.getUserId());
        logDO.setUserType(log.getUserType());
        logDO.setUserNickname(log.getUserNickname());
        logDO.setCostTime(log.getCostTime());
        logDO.setSuccess(log.getSuccess());
        logDO.setErrorMessage(log.getErrorMessage());
        logDO.setTraceId(log.getTraceId());
        logDO.setRequestIp(log.getRequestIp());
        logDO.setRequestMethod(log.getRequestMethod());
        logDO.setRequestUrl(log.getRequestUrl());
        logDO.setTenantId(TenantContextHolder.getTenantId());
        return logDO;
    }

    /**
     * 驼峰转下划线（简化版）
     */
    private String camelToUnderscore(String camelCase) {
        if (StrUtil.isBlank(camelCase)) {
            return camelCase;
        }
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < camelCase.length(); i++) {
            char c = camelCase.charAt(i);
            if (Character.isUpperCase(c) && i > 0) {
                result.append('_');
            }
            result.append(Character.toLowerCase(c));
        }
        return result.toString();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 预留扩展
    }

    /**
     * 内部日志对象，避免直接依赖 DO
     */
    @Data
    private static class DatabaseOperateLog {
        private String operateType;
        private String tableName;
        private String primaryKey;
        private String primaryKeyValue;
        private String beforeData;
        private String afterData;
        private String sqlStatement;
        private String sqlParams;
        private Long userId;
        private Integer userType;
        private String userNickname;
        private Long costTime;
        private Boolean success;
        private String errorMessage;
        private String traceId;
        private String requestIp;
        private String requestMethod;
        private String requestUrl;
        private Long tenantId;
    }
}


