package com.ruoyi.framework.interceptor.mybatis;

import com.ruoyi.system.domain.changerecord.enums.OperationType;
import com.ruoyi.system.domain.fieldchangerecord.DataFieldChangeRecord;
import com.ruoyi.system.domain.fieldchangerecord.annotation.DataFieldChangeLog;
import com.ruoyi.system.domain.fieldchangerecord.enums.FieldCode;
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.math.BigDecimal;
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.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 字段级变更日志拦截器。
 *
 * <p>和 {@link DataChangeLogInterceptor} 并行工作，拦截
 * {@link Executor#update(MappedStatement, Object)} 调用，读取 Mapper 方法上的
 * {@link DataFieldChangeLog} 注解，对声明的字段做前后值 diff 后写入
 * {@code data_field_change_record} 表。</p>
 *
 * <p>关键约束：</p>
 * <ol>
 *     <li>只处理 {@link OperationType#INSERT} 和 {@link OperationType#UPDATE}，
 *         DELETE 直接跳过（继续走主体级日志）。</li>
 *     <li>UPDATE 场景必须配置 {@code preLoadMapper} 捞旧记录；否则无法 diff 直接跳过。</li>
 *     <li><strong>Partial update 语义</strong>：入参里字段值为 null 视为"未修改"，不 diff、不写日志。</li>
 *     <li>受影响行数为 0 时跳过日志写入。</li>
 *     <li>INSERT 且字段值为 null 时不写日志（避免一条 insert 炸出 N 条空日志）。</li>
 *     <li>{@link FieldCode#BE_YEAR_SELL} 的值格式化为 {@code "year=2025;value=523.50"}。</li>
 *     <li>BigDecimal 比较采用 {@code compareTo}，避免 scale 不同导致误判（1.0 vs 1.00）。</li>
 *     <li>日志写入失败抛出异常，业务事务整体回滚。</li>
 * </ol>
 */
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class DataFieldChangeLogInterceptor implements Interceptor {

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

    private static final String LOG_INSERT_STATEMENT_ID =
            "com.ruoyi.system.mapper.fieldchangerecord.DataFieldChangeRecordMapper.batchInsert";

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

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

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

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

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

        // UPDATE 场景先捞旧值，按 preLoadMapper 缓存避免重复查询。
        Map<String, Map<String, Object>> preloadByMapper = runPreload(annotations, param, executor, ms);

        Object result = invocation.proceed();

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

        List<DataFieldChangeRecord> records = buildRecords(annotations, param, preloadByMapper);
        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_field_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 DataFieldChangeLog[] resolveAnnotations(String statementId) {
        return annotationCache.computeIfAbsent(statementId, this::loadAnnotations);
    }

    private DataFieldChangeLog[] 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;
                }
                DataFieldChangeLog[] annos = m.getAnnotationsByType(DataFieldChangeLog.class);
                if (annos.length > 0) {
                    return annos;
                }
            }
        } catch (ClassNotFoundException ignore) {
        }
        return EMPTY;
    }

    /**
     * 对所有 UPDATE 注解执行 preload 查询，返回 {@code preLoadMapperFullName -> pk -> oldEntity} 三层结构。
     * 同一 preLoadMapper 只查一次。
     */
    private Map<String, Map<String, Object>> runPreload(DataFieldChangeLog[] annotations, Object param,
                                                        Executor executor, MappedStatement ms) throws Exception {
        Map<String, Map<String, Object>> results = new HashMap<>();
        for (DataFieldChangeLog anno : annotations) {
            if (anno.operation() != OperationType.UPDATE) {
                continue;
            }
            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);
            Map<String, Object> byPk = new HashMap<>();
            if (queryResult != null) {
                for (Object old : queryResult) {
                    if (old == null) {
                        continue;
                    }
                    Object pk = getProperty(old, "id");
                    if (pk != null) {
                        byPk.put(String.valueOf(pk), old);
                    }
                }
            }
            results.put(preLoad, byPk);
        }
        return results;
    }

    private List<DataFieldChangeRecord> buildRecords(DataFieldChangeLog[] annotations, Object param,
                                                     Map<String, Map<String, Object>> preloadByMapper) {
        Date now = new Date();
        List<DataFieldChangeRecord> records = new ArrayList<>();
        for (DataFieldChangeLog anno : annotations) {
            if (anno.operation() != OperationType.INSERT && anno.operation() != OperationType.UPDATE) {
                continue;
            }
            // UPDATE 必须配 preLoadMapper，否则无法 diff。
            Map<String, Object> oldByPk = Collections.emptyMap();
            if (anno.operation() == OperationType.UPDATE) {
                if (anno.preLoadMapper().isEmpty()) {
                    log.warn("@DataFieldChangeLog UPDATE 未配 preLoadMapper，跳过 diff。subjectType={}", anno.subjectType());
                    continue;
                }
                oldByPk = preloadByMapper.getOrDefault(anno.preLoadMapper(), Collections.emptyMap());
            }

            List<Subject> subjects = resolveSubjects(param, anno.idExpr(), oldByPk);
            for (Subject subject : subjects) {
                if (subject.subjectId == null || subject.subjectId.isEmpty() || subject.source == null) {
                    continue;
                }
                Object sourcePk = getProperty(subject.source, "id");
                Object oldSnapshot = sourcePk == null ? null : oldByPk.get(String.valueOf(sourcePk));

                for (FieldCode field : anno.fields()) {
                    DataFieldChangeRecord rec = buildOneRecord(anno, subject, oldSnapshot, field, now);
                    if (rec != null) {
                        records.add(rec);
                    }
                }
            }
        }
        return records;
    }

    private DataFieldChangeRecord buildOneRecord(DataFieldChangeLog anno, Subject subject,
                                                 Object oldSnapshot, FieldCode field, Date now) {
        Object newRaw = getProperty(subject.source, field.getPropertyName());
        Object oldRaw = oldSnapshot == null ? null : getProperty(oldSnapshot, field.getPropertyName());

        if (anno.operation() == OperationType.INSERT) {
            if (newRaw == null) {
                return null;
            }
        } else {
            // UPDATE：partial update 语义——newRaw 为 null 视为未修改。
            if (newRaw == null) {
                return null;
            }
            if (valuesEqual(newRaw, oldRaw)) {
                return null;
            }
        }

        String oldStr;
        String newStr;
        if (field == FieldCode.BE_YEAR_SELL) {
            Object newYear = getProperty(subject.source, "year");
            Object oldYear = oldSnapshot == null ? null : getProperty(oldSnapshot, "year");
            newStr = formatYearSell(newYear, newRaw);
            oldStr = (oldSnapshot == null || oldRaw == null) ? null : formatYearSell(oldYear, oldRaw);
        } else {
            newStr = String.valueOf(newRaw);
            oldStr = oldRaw == null ? null : String.valueOf(oldRaw);
        }

        DataFieldChangeRecord r = new DataFieldChangeRecord();
        r.setId(UUID.randomUUID().toString().replace("-", ""));
        r.setSubjectType(anno.subjectType().name());
        r.setSubjectId(subject.subjectId);
        r.setFieldCode(field.name());
        r.setOldValue(oldStr);
        r.setNewValue(newStr);
        r.setOperationType(anno.operation().getCode());
        r.setOperationTime(now);
        return r;
    }

    private String formatYearSell(Object year, Object value) {
        return "year=" + (year == null ? "" : String.valueOf(year))
                + ";value=" + (value == null ? "" : String.valueOf(value));
    }

    /**
     * 解析 idExpr，返回 (subjectId, 值源对象) 列表。
     *
     * <p>值源对象用于后续反射取字段新值。对于 {@code list[*].xxx} 批量表达式，
     * 每个列表元素分别作为独立的值源。</p>
     *
     * <p>特殊：{@code @preload[*].xxx} 用于 partial update 场景（例如
     * {@code updateBusinessEntitySell} 入参不含 {@code businessEntityInfoId} 时，
     * 需要从预加载的旧记录拿 subject_id）。此时 source 仍是 {@code param}，因为字段
     * 新值必然来自业务写操作的入参，而 pk 匹配依然能将 source 和 oldSnapshot 挂钩。
     * 该特殊路径只适用于"单实体 update"场景（preload 结果大小为 1）。</p>
     */
    private List<Subject> resolveSubjects(Object param, String idExpr, Map<String, Object> oldByPk) {
        if (idExpr == null || idExpr.isEmpty() || param == null) {
            return Collections.emptyList();
        }

        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);
            }
            if ("@preload".equals(listKey)) {
                if (oldByPk == null || oldByPk.isEmpty()) {
                    return Collections.emptyList();
                }
                List<Subject> subjects = new ArrayList<>(oldByPk.size());
                for (Object old : oldByPk.values()) {
                    Object idVal = afterStar.isEmpty() ? old : getProperty(old, afterStar);
                    if (idVal == null) {
                        continue;
                    }
                    subjects.add(new Subject(String.valueOf(idVal), param));
                }
                return subjects;
            }
            Object v = getProperty(param, listKey);
            if (!(v instanceof List)) {
                return Collections.emptyList();
            }
            List<?> list = (List<?>) v;
            List<Subject> subjects = new ArrayList<>(list.size());
            for (Object item : list) {
                if (item == null) {
                    continue;
                }
                Object idVal = afterStar.isEmpty() ? item : getProperty(item, afterStar);
                if (idVal == null) {
                    continue;
                }
                subjects.add(new Subject(String.valueOf(idVal), item));
            }
            return subjects;
        }

        Object val = getProperty(param, idExpr);
        if (val == null) {
            return Collections.emptyList();
        }
        return Collections.singletonList(new Subject(String.valueOf(val), param));
    }

    private boolean valuesEqual(Object a, Object b) {
        if (a == null && b == null) {
            return true;
        }
        if (a == null || b == null) {
            return false;
        }
        if (a instanceof BigDecimal && b instanceof BigDecimal) {
            return ((BigDecimal) a).compareTo((BigDecimal) b) == 0;
        }
        return Objects.equals(a, b);
    }

    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 capitalized = Character.toUpperCase(key.charAt(0)) + key.substring(1);
            String getter = "get" + capitalized;
            String booleanGetter = "is" + capitalized;
            for (Method m : obj.getClass().getMethods()) {
                if (m.getParameterCount() != 0) {
                    continue;
                }
                if (m.getName().equals(getter) || m.getName().equals(booleanGetter)) {
                    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;
    }

    /** 一个 (subject_id, 值源对象) 二元组。 */
    private static final class Subject {
        final String subjectId;
        final Object source;

        Subject(String subjectId, Object source) {
            this.subjectId = subjectId;
            this.source = source;
        }
    }
}
