package com.hdkj.lt.core.plugins.security;

import com.hdkj.lt.base.annotations.MybatisLikeInterceptorMethod;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * mybatis/mybatis-plus模糊查询语句特殊字符转义拦截器
 *
 * @author ZY
 */
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
@Component
public class MybatisLikeSqlInterceptor implements Interceptor {

    /**
     * SQL语句like
     */
    private final static String SQL_LIKE = " like ";

    /**
     * SQL语句占位符
     */
    private final static String SQL_PLACEHOLDER = "?";

    /**
     * SQL语句占位符分隔
     */
    private final static String SQL_PLACEHOLDER_REGEX = "\\?";

    /**
     * 判断sql语句中like ?常用正则表达式
     */
    private final Pattern LIKE_PATTERN = Pattern.compile("like[\\s]+(concat[\\s]*\\([\\s]*(['\"]%['\"][\\s]*,)*[\\s]*)?\\?");

    /**
     * SQL是否有ESCAPE转义关键字符
     */
    private final Pattern SQL_ESCAPE_PATTERN = Pattern.compile("[\\s]+(escape)[\\s]+('/')|(\"/\")");

    /**
     * 所有的转义器
     */
    private static Map<Class, AbstractLikeSqlConverter> converterMap = new HashMap<>(4);

    /**
     * 执行分页COUNT查询时的主方法名称
     */
    private ThreadLocal<String> pageMainMethodName = new ThreadLocal<>();

    static {
        converterMap.put(Map.class, new MapLikeSqlConverter());
        converterMap.put(Object.class, new ObjectLikeSqlConverter());
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement statement = (MappedStatement) args[0];
        if (Objects.nonNull(this.getAnnotation(statement))) {
            Object parameterObject = args[1];
            BoundSql boundSql = statement.getBoundSql(parameterObject);
            String sql = boundSql.getSql();
            this.transferLikeSql(sql, parameterObject, boundSql);
        }
        return invocation.proceed();
    }

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

    @Override
    public void setProperties(Properties arg0) {
        // Do nothing because of X and Y.
    }

    /**
     * 修改包含like的SQL语句
     *
     * @param sql             SQL语句
     * @param parameterObject 参数对象
     * @param boundSql        绑定SQL对象
     */
    private void transferLikeSql(String sql, Object parameterObject, BoundSql boundSql) {
        if (!isEscape(sql)) {
            return;
        }
        sql = sql.replaceAll(" {2,}", " ");
        // 获取关键字的个数（去重）
        Set<String> fields = this.getKeyFields(sql, boundSql);
        if (fields.isEmpty()) {
            return;
        }
        // 此处可以增强,不止是支持Map对象,Map对象仅用于传入的条件为Map或者使用@Param传入的对象被Mybatis转为的Map
        AbstractLikeSqlConverter converter;
        // 对关键字进行特殊字符“清洗”，如果有特殊字符的，在特殊字符前添加转义字符（\）
        if (parameterObject instanceof Map) {
            converter = converterMap.get(Map.class);
        } else {
            converter = converterMap.get(Object.class);
        }
        converter.setHasEscapeKeywords(this.hasEscapeKeywords(sql));
        converter.convert(sql, fields, parameterObject);
    }

    /**
     * 是否需要转义
     *
     * @param sql SQL语句
     * @return true/false
     */
    private boolean isEscape(String sql) {
        // 准确判断sql是否为like ?模式
        Matcher matcher = LIKE_PATTERN.matcher(sql.toLowerCase());
        return this.hasLike(sql) && this.hasPlaceholder(sql) && matcher.find();
    }

    /**
     * 判断SQL语句中是否含有like关键字
     *
     * @param str SQL语句
     * @return true/false
     */
    private boolean hasLike(String str) {
        if (StringUtils.isBlank(str)) {
            return false;
        }
        return str.toLowerCase().contains(SQL_LIKE);
    }

    /**
     * 判断SQL语句中是否包含SQL占位符
     *
     * @param str SQL语句
     * @return true/false
     */
    private boolean hasPlaceholder(String str) {
        if (StringUtils.isBlank(str)) {
            return false;
        }
        return str.toLowerCase().contains(SQL_PLACEHOLDER);
    }

    /**
     * 判断SQL语句中是否包含转义关键字`ESCAPE`
     *
     * @param str SQL语句
     * @return true/false
     */
    private boolean hasEscapeKeywords(String str) {
        if (StringUtils.isBlank(str)) {
            return false;
        }
        // 是否包含转义关键字`ESCAPE`
        Matcher matcher = SQL_ESCAPE_PATTERN.matcher(str.toLowerCase());
        return matcher.find();
    }


    /**
     * 获取需要替换的所有字段集合
     *
     * @param sql      完整SQL语句
     * @param boundSql 绑定的SQL对象
     * @return 字段集合列表
     */
    private Set<String> getKeyFields(String sql, BoundSql boundSql) {
        String[] params = sql.split(SQL_PLACEHOLDER_REGEX);
        Set<String> fields = new HashSet<>();
        for (int i = 0; i < params.length; i++) {
            if (this.hasLike(params[i])) {
                String field = boundSql.getParameterMappings().get(i).getProperty();
                fields.add(field);
            }
        }
        return fields;
    }

    /**
     * 获取注解MybatisLike拦截器注解
     */
    private Object getAnnotation(MappedStatement statement) {
        try {
            String id = statement.getId();
            String className = id.substring(0, id.lastIndexOf("."));
            String methodName = id.replace(className, "").replace(".", "");
            MybatisLikeInterceptorMethod annotation = null;
            // 当分页主方法不为空且和执行主方法时不处理参数（分页方法已经处理）
            if (StringUtils.equals(pageMainMethodName.get(), methodName)) {
                pageMainMethodName.remove();
                return annotation;
            }
            Method[] methods = Class.forName(className).getMethods();
            Class<MybatisLikeInterceptorMethod> clazz = MybatisLikeInterceptorMethod.class;
            for (Method method : methods) {
                if (StringUtils.endsWith(methodName, "_mpCount")) {
                    // 查询时会有自动形成的方法，查分页page的total方法，此时没有注解开关，需要处理一下
                    methodName = StringUtils.replace(methodName, "_mpCount", "");
                    pageMainMethodName.set(methodName); // 设置分页方法
                }
                if (StringUtils.equals(method.getName(), methodName) && method.isAnnotationPresent(clazz)) {
                    annotation = method.getAnnotation(clazz);
                    break;
                }
            }
            return annotation;
        } catch (ClassNotFoundException e) {
            pageMainMethodName.remove();
            return null;
        }
    }

}
