package com.ruoyi.framework.interceptor.mybatis;

import com.ruoyi.system.domain.changerecord.DataChangeRecord;
import com.ruoyi.system.domain.changerecord.annotation.DataChangeLog;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 数据变更日志拦截器。
 *
 * <p>拦截所有 {@link Executor#update(MappedStatement, Object)} 调用，读取 Mapper 方法上的
 * {@link DataChangeLog} 注解，解析出 subject_id 后向 {@code data_change_record} 表写日志。</p>
 *
 * <p>关键约束：</p>
 * <ol>
 *     <li>未标注 {@code @DataChangeLog} 的 Mapper 方法不会产生任何额外开销。</li>
 *     <li>日志写入与原业务 SQL 共用同一个 {@link Executor}，天然在同一事务内，失败会一起回滚。</li>
 *     <li>批量写操作会按 {@code list[*].id} 表达式展开成多条日志。</li>
 *     <li>预加载场景：{@code @DataChangeLog(preLoadMapper=...)} 可在写操作前先执行一次查询
 *         （如 mapping 按 house_resource_id 删除时，需要先查出被影响的 business_entity_info_id）。</li>
 *     <li>受影响行数为 0 时跳过日志写入，避免"查不到的更新"污染统计。</li>
 * </ol>
 */
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class DataChangeLogInterceptor implements Interceptor {

    private static final Logger log = LoggerFactory.getLogger(DataChangeLogInterceptor.class);

    private static final String LOG_INSERT_STATEMENT_ID =
            "com.ruoyi.system.mapper.changerecord.DataChangeRecordMapper.batchInsert";

    private static final DataChangeLog[] EMPTY = new DataChangeLog[0];

    /** 按 MappedStatement id 缓存 @DataChangeLog 列表。 */
    private final Map<String, DataChangeLog[]> annotationCache = new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object param = args[1];

        DataChangeLog[] annotations = resolveAnnotations(ms.getId());
        if (annotations.length == 0) {
            return invocation.proceed();
        }

        Executor executor = (Executor) invocation.getTarget();

        Map<String, List<?>> preloadResults = runPreload(annotations, param, executor, ms);

        Object result = invocation.proceed();

        if (result instanceof Integer && (Integer) result == 0) {
            return result;
        }

        List<DataChangeRecord> records = buildRecords(annotations, param, preloadResults);
        if (records.isEmpty()) {
            return result;
        }

        try {
            MappedStatement logMs = ms.getConfiguration().getMappedStatement(LOG_INSERT_STATEMENT_ID);
            Map<String, Object> logParam = new HashMap<>(2);
            logParam.put("list", records);
            executor.update(logMs, logParam);
        } catch (Exception e) {
            log.error("写入 data_change_record 失败，业务事务将回滚。ms={}", ms.getId(), e);
            throw e;
        }

        return result;
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return org.apache.ibatis.plugin.Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(java.util.Properties properties) {
    }

    private DataChangeLog[] resolveAnnotations(String statementId) {
        return annotationCache.computeIfAbsent(statementId, this::loadAnnotations);
    }

    private DataChangeLog[] loadAnnotations(String statementId) {
        int dot = statementId.lastIndexOf('.');
        if (dot <= 0) {
            return EMPTY;
        }
        String mapperClassName = statementId.substring(0, dot);
        String methodName = statementId.substring(dot + 1);
        try {
            Class<?> mapperClass = Class.forName(mapperClassName);
            for (Method m : mapperClass.getDeclaredMethods()) {
                if (!m.getName().equals(methodName)) {
                    continue;
                }
                DataChangeLog[] annos = m.getAnnotationsByType(DataChangeLog.class);
                if (annos.length > 0) {
                    return annos;
                }
            }
        } catch (ClassNotFoundException ignore) {
        }
        return EMPTY;
    }

    private Map<String, List<?>> runPreload(DataChangeLog[] annotations, Object param, Executor executor,
                                            MappedStatement ms) throws Exception {
        Map<String, List<?>> results = new HashMap<>();
        for (DataChangeLog anno : annotations) {
            String preLoad = anno.preLoadMapper();
            if (preLoad.isEmpty() || results.containsKey(preLoad)) {
                continue;
            }
            MappedStatement preLoadMs = ms.getConfiguration().getMappedStatement(preLoad);
            @SuppressWarnings("rawtypes")
            List queryResult = executor.query(preLoadMs, param, RowBounds.DEFAULT,
                    (ResultHandler) null);
            results.put(preLoad, queryResult);
        }
        return results;
    }

    private List<DataChangeRecord> buildRecords(DataChangeLog[] annotations, Object param,
                                                Map<String, List<?>> preloadResults) {
        Date now = new Date();
        List<DataChangeRecord> records = new ArrayList<>();
        for (DataChangeLog anno : annotations) {
            List<?> preload = anno.preLoadMapper().isEmpty() ? null : preloadResults.get(anno.preLoadMapper());
            List<String> subjectIds = resolveIds(param, anno.idExpr(), preload);
            for (String subjectId : subjectIds) {
                if (subjectId == null || subjectId.isEmpty()) {
                    continue;
                }
                DataChangeRecord r = new DataChangeRecord();
                r.setId(UUID.randomUUID().toString().replace("-", ""));
                r.setSubjectType(anno.subjectType().name());
                r.setSubjectId(subjectId);
                r.setOperationType(anno.operation().getCode());
                r.setOperationTime(now);
                records.add(r);
            }
        }
        return records;
    }

    private List<String> resolveIds(Object param, String idExpr, List<?> preload) {
        if (idExpr == null || idExpr.isEmpty()) {
            return Collections.emptyList();
        }
        if (".".equals(idExpr)) {
            return param == null ? Collections.emptyList()
                    : Collections.singletonList(String.valueOf(param));
        }

        int starIdx = idExpr.indexOf("[*]");
        if (starIdx >= 0) {
            String listKey = idExpr.substring(0, starIdx);
            String afterStar = idExpr.substring(starIdx + 3);
            if (afterStar.startsWith(".")) {
                afterStar = afterStar.substring(1);
            }

            List<?> list;
            if ("@preload".equals(listKey)) {
                list = preload;
            } else {
                Object v = getProperty(param, listKey);
                list = (v instanceof List) ? (List<?>) v : null;
            }
            if (list == null || list.isEmpty()) {
                return Collections.emptyList();
            }
            List<String> ids = new ArrayList<>(list.size());
            for (Object item : list) {
                Object idVal = afterStar.isEmpty() ? item : getProperty(item, afterStar);
                if (idVal != null) {
                    ids.add(String.valueOf(idVal));
                }
            }
            return ids;
        }

        Object val = getProperty(param, idExpr);
        return val == null ? Collections.emptyList() : Collections.singletonList(String.valueOf(val));
    }

    private Object getProperty(Object obj, String key) {
        if (obj == null || key == null || key.isEmpty()) {
            return null;
        }
        if (obj instanceof Map) {
            return ((Map<?, ?>) obj).get(key);
        }
        try {
            String getter = "get" + Character.toUpperCase(key.charAt(0)) + key.substring(1);
            for (Method m : obj.getClass().getMethods()) {
                if (m.getName().equals(getter) && m.getParameterCount() == 0) {
                    return m.invoke(obj);
                }
            }
            Field f = findField(obj.getClass(), key);
            if (f != null) {
                f.setAccessible(true);
                return f.get(obj);
            }
        } catch (Exception ignore) {
        }
        return null;
    }

    private Field findField(Class<?> cls, String name) {
        Class<?> c = cls;
        while (c != null && c != Object.class) {
            for (Field f : c.getDeclaredFields()) {
                if (f.getName().equals(name)) {
                    return f;
                }
            }
            c = c.getSuperclass();
        }
        return null;
    }
}
