package com.trade.common.config.mybatis.plugin;

import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.*;
import java.util.stream.Collectors;

import cn.hutool.core.collection.CollUtil;
import com.trade.common.core.domain.BaseI18nEntity;
import com.trade.common.exception.ServiceException;
import com.trade.common.utils.I18nUtil;
import com.trade.common.utils.MapUtil;
import com.trade.common.utils.ReflectionUtil;
import com.trade.common.utils.SqlExecutionUtil;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.apache.ibatis.binding.MapperMethod;
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.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 拼接sql方式i18n处理拦截器
 */
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {
                MappedStatement.class, Object.class
        }), @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
        })
})
public class I18nSqlProcessInterceptor implements Interceptor {

    public static final Logger log = LoggerFactory.getLogger(I18nSqlProcessInterceptor.class);
    private static final String ID_CONSTANT = "id";
    private static final String LANGUAGE_CONSTANT = "language";
    private static final String VALUE_CONSTANT = "value";
    private static final String DELIMITER_DOT = ".";
    private static final EnumMap<SqlCommandType, String[]> supportedOperationMap = new EnumMap<>(SqlCommandType.class);
    // 支持的mp的method集合
    static {
        supportedOperationMap.put(SqlCommandType.INSERT, new String[] {
                SqlMethod.INSERT_ONE.getMethod()
        });
        supportedOperationMap.put(SqlCommandType.UPDATE, new String[] {
                SqlMethod.UPDATE.getMethod(), SqlMethod.UPDATE_BY_ID.getMethod()
        });
        supportedOperationMap.put(SqlCommandType.DELETE, new String[] {
                SqlMethod.DELETE.getMethod(), SqlMethod.DELETE_BY_ID.getMethod(), SqlMethod.DELETE_BY_MAP.getMethod()
        });
        supportedOperationMap.put(SqlCommandType.SELECT, new String[] {
                SqlMethod.SELECT_BY_ID.getMethod(), SqlMethod.SELECT_LIST.getMethod(), SqlMethod.SELECT_ONE.getMethod(),
                SqlMethod.SELECT_MAPS.getMethod(), SqlMethod.SELECT_PAGE.getMethod(),
                SqlMethod.SELECT_MAPS_PAGE.getMethod()
        });
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object baseResult;
        // 初始化
        // 参数对象
        Object parameter = invocation.getArgs()[1];
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        // 基本方法名
        String baseMethodStr = ms.getId().substring(ms.getId().lastIndexOf(DELIMITER_DOT) + 1);
        Class parameterClass = parameter != null ? parameter.getClass() : null;
        Executor executor = (Executor) invocation.getTarget();
        Connection connection = executor.getTransaction().getConnection();
        Class domainClass = null;
        TableInfo tableInfo = null;
        try {
            domainClass = ReflectionUtil
                    .extractModelClass(Class.forName(ms.getId().substring(0, ms.getId().lastIndexOf(DELIMITER_DOT))));
            if (domainClass != null) {
                tableInfo = TableInfoHelper.getTableInfo(domainClass);
            }
        } catch (ClassNotFoundException e) {
            log.warn("Mapper: {}, not use mybatis-plus  way of injection Mapper,not find entity class", ms.getId());
        }
        // 不采用mp的mapper使用方式情况
        if (domainClass == null || tableInfo == null) {
            return invocation.proceed();
        }
        switch (ms.getSqlCommandType()) {
            case INSERT: {
                // 先执行,否则拿不到id
                baseResult = invocation.proceed();
                if (methodSupported(SqlCommandType.INSERT, baseMethodStr)
                        && BaseI18nEntity.class.isAssignableFrom(domainClass)) {
                    List<String> i18nFieldNameList = I18nUtil.getSpecificAnnotationI18nFieldNameList(parameterClass);
                    if (BaseI18nEntity.class.isAssignableFrom(parameterClass)
                            && CollectionUtils.isNotEmpty(i18nFieldNameList)) {
                        String baseTableName = tableInfo.getTableName();
                        Map<String, Map<String, String>> metaDataMap = constructMetaDataMap(invocation.getArgs()[1],
                                i18nFieldNameList);
                        execInsertOrUpdateList(metaDataMap, baseTableName, connection,
                                (Long) ReflectionUtil.getMethodValue(parameter, ID_CONSTANT));
                    }
                }
                break;
            }
            case UPDATE: {
                BoundSql boundSql = ms.getSqlSource().getBoundSql(parameter);
                if (methodSupported(SqlCommandType.UPDATE, baseMethodStr)
                        && BaseI18nEntity.class.isAssignableFrom(domainClass)) {
                    if (Map.class.isAssignableFrom(boundSql.getParameterObject().getClass())
                            && Map.class.isAssignableFrom(parameterClass)) {
                        MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) boundSql.getParameterObject();
                        if (parameterMap.containsKey("ew")) {
                            // update method
                            Object entityWrapper = parameterMap.get("ew");
                            // update 不带where 直接返回原逻辑
                            if (entityWrapper == null) {
                                return invocation.proceed();
                            }
                            Object entity = ((AbstractWrapper) entityWrapper).getEntity();
                            Map<String, String> paramValuePairs = ((AbstractWrapper) entityWrapper)
                                    .getParamNameValuePairs();
                            List<Long> idList;
                            List<Object> valueList = new ArrayList<>();
                            // 针对ew使用姿势不同,做的两种区分
                            if (entity != null) {
                                List<ParameterMapping> parameterMappingList = boundSql.getParameterMappings();
                                List<String> parametersStrList = parameterMappingList.stream()
                                        .map(ParameterMapping::getProperty).collect(Collectors.toList());
                                // 只取ew.entity.XXX
                                valueList.addAll(parametersStrList.stream().filter(s -> s.contains("ew.entity"))
                                        .map(s -> s.substring(s.lastIndexOf(DELIMITER_DOT) + 1))
                                        .collect(Collectors.toList()).stream()
                                        .map(p -> ReflectionUtil.getMethodValue(entity, p))
                                        .collect(Collectors.toList()));
                            }
                            if (!paramValuePairs.isEmpty()) {
                                Map<String, String> sortedMap = MapUtil.sortMapByAlphaNumKey(paramValuePairs);
                                valueList.addAll(new ArrayList<>(sortedMap.values()));
                            }
                            String conditionStr = getSqlFromBaseSql(tableInfo, boundSql.getSql(), SqlCommandType.UPDATE,
                                    null, null, null);
                            String selectIdSb = "SELECT id FROM " + tableInfo.getTableName() + " " + conditionStr + ";";
                            idList = SqlExecutionUtil.executeForIdsWithParameters(connection, valueList, selectIdSb);
                            if (CollectionUtils.isNotEmpty(idList)) {
                                // insertOrUpdate 根据传入的i18n元数据插入i18n表
                                Object baseEntity = parameterMap.get("et");
                                Class baseEntityClass = baseEntity.getClass();
                                TableInfo finalTableInfo = tableInfo;
                                idList.forEach(id -> {
                                    List<String> i18nFieldNameList = I18nUtil
                                            .getSpecificAnnotationI18nFieldNameList(baseEntityClass);
                                    Map<String, Map<String, String>> metaDataMap = constructMetaDataMap(baseEntity,
                                            i18nFieldNameList);
                                    execInsertOrUpdateList(metaDataMap, finalTableInfo.getTableName(), connection, id);
                                });
                            }
                        } else if (parameterMap.containsKey("et")
                                && BaseI18nEntity.class.isAssignableFrom(parameterMap.get("et").getClass())) {
                                    // updateById,updateAllColumnsById method
                                    Object baseEntity = parameterMap.get("et");
                                    String baseTableName = tableInfo.getTableName();
                                    Long baseTableId = (Long) ReflectionUtil.getMethodValue(baseEntity, ID_CONSTANT);
                                    // updateById,updateAllColumnsById传入的entity必须有id,不然无法确定改哪个
                                    if (baseTableId != null) {
                                        List<String> i18nFieldNameList = I18nUtil
                                                .getSpecificAnnotationI18nFieldNameList(baseEntity.getClass());
                                        Map<String, Map<String, String>> metaDataMap = constructMetaDataMap(baseEntity,
                                                i18nFieldNameList);
                                        execInsertOrUpdateList(metaDataMap, baseTableName, connection, baseTableId);
                                    } else {
                                        log.info("Can not find the value of id of parameter 'entity'!");
                                    }
                                }
                    } else {
                        log.info("Parameter'class: " + parameterClass.getName()
                                + ",i18n interceptor is not supported for this api now!");
                    }
                }
                baseResult = invocation.proceed();
                break;
            }
            // 只有物理删除才走删除i18n，逻辑删除时i18n不动
            case DELETE: {
                if (methodSupported(SqlCommandType.DELETE, baseMethodStr)
                        && BaseI18nEntity.class.isAssignableFrom(domainClass)) {
                    if (Map.class.isAssignableFrom(parameterClass)) {
                        BoundSql boundSql = ms.getSqlSource().getBoundSql(parameter);
                        HashMap parameterMap = (HashMap) boundSql.getParameterObject();
                        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappings();
                        List<Object> valueList = new ArrayList<>();
                        if (parameterMap.containsKey("ew")) {
                            // delete 不带where 直接返回原逻辑
                            Object entityWrapper = parameterMap.get("ew");
                            if (entityWrapper == null) {
                                return invocation.proceed();
                            }
                            Object entity = ((AbstractWrapper) entityWrapper).getEntity();
                            List<String> parametersStrList = parameterMappingList.stream()
                                    .map(s -> s.getProperty().substring(s.getProperty().lastIndexOf(DELIMITER_DOT) + 1))
                                    .collect(Collectors.toList());
                            valueList = parametersStrList.stream().map(p -> ReflectionUtil.getMethodValue(entity, p))
                                    .collect(Collectors.toList());
                        } else if (parameterMap.containsKey("cm")) {
                            HashMap<String, Object> entity = (HashMap) parameterMap.get("cm");
                            List<String> parametersStrList = parameterMappingList.stream().map(s -> s.getProperty()
                                    .substring(s.getProperty().indexOf("[") + 1, s.getProperty().indexOf("]")))
                                    .collect(Collectors.toList());
                            valueList = parametersStrList.stream().map(p -> entity.get(p)).collect(Collectors.toList());
                        }
                        String deleteSql = getSqlFromBaseSql(tableInfo, boundSql.getSql(), SqlCommandType.DELETE, null,
                                null, null);
                        SqlExecutionUtil.executeForNoResultWithManyParameters(connection, deleteSql, valueList);
                    } else if (Long.class.isAssignableFrom(parameterClass)) {
                        BoundSql boundSql = ms.getSqlSource().getBoundSql(parameter);
                        Long id = (Long) boundSql.getParameterObject();
                        String deleteSql = getSqlFromBaseSql(tableInfo, boundSql.getSql(), SqlCommandType.DELETE, null,
                                null, null);
                        SqlExecutionUtil.executeForNoResultWithParameterId(connection, deleteSql, id);
                    } else {
                        log.info("parameter'class: " + parameterClass.getName()
                                + ",i18n interceptor is not supported for this api now!");
                    }
                }
                // 后执行,否则delete关联时候有问题
                baseResult = invocation.proceed();
                break;
            }
            case SELECT: {
                baseResult = invocation.proceed();
                if (methodSupported(SqlCommandType.SELECT, baseMethodStr)
                        && BaseI18nEntity.class.isAssignableFrom(domainClass)) {
                    if (Map.class.isAssignableFrom(parameterClass)) {
                        BoundSql boundSql = ms.getSqlSource().getBoundSql(parameter);
                        MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) boundSql.getParameterObject();
                        Object ew = parameterMap.get("ew");
                        if (ew instanceof Non18nWrapper) {
                            return baseResult;
                        }
                    }
                    if (baseResult instanceof List && !((List<?>) baseResult).isEmpty()) {
                        I18nUtil.setI18nMap((List) baseResult, domainClass, tableInfo, connection);
                    }
                }
                break;
            }
            default:
                throw new ServiceException("非CRUD操作,请检查! ");
        }
        return baseResult;
    }

    /**
     * 根据sqlCommondType分类 依赖tableFieldInfoList、i18nFieldList加工baseSql
     *
     * @param baseSql 原始Sql
     * @param sqlCommandType 原始CommandType
     * @param tableFieldInfoList 原始tableFieldInfo
     * @param i18nFieldList i18nField注解标识的属性集合
     * @param sqlWhere 针对select的一种特殊sql处理
     * @return
     */
    private String getSqlFromBaseSql(TableInfo tableInfo, String baseSql, SqlCommandType sqlCommandType,
            List<TableFieldInfo> tableFieldInfoList, List<String> i18nFieldList, String sqlWhere) {
        StringBuilder sb = new StringBuilder();
        switch (sqlCommandType) {
            case SELECT: {
                baseSql = baseSql.replaceAll("`", "");
                if (StringUtils.isEmpty(sqlWhere)) {
                    for (TableFieldInfo tableFieldInfo : tableFieldInfoList) {
                        if (i18nFieldList.contains(tableFieldInfo.getProperty())) {
                            baseSql = replaceColumnWithTableAlias(baseSql,
                                    tableFieldInfo.getColumn().replaceAll("`", ""), "i18n",
                                    "base." + tableFieldInfo.getColumn() + " AS base_" + tableFieldInfo.getProperty()
                                            + ",");
                        } else {
                            baseSql = replaceColumnWithTableAlias(baseSql,
                                    tableFieldInfo.getColumn().replaceAll("`", ""), "base", "");
                        }
                    }
                    // id特殊处理
                    baseSql = replaceColumnWithTableAlias(baseSql, "id", "base", "");
                    sb.append(baseSql, 0, baseSql.indexOf("WHERE")).append("base LEFT JOIN ")
                            .append(tableInfo.getTableName()).append("_i18n i18n ON base.id = i18n.id ")
                            .append(replaceColumnWithTableAlias(baseSql.substring(baseSql.indexOf("WHERE")), "id",
                                    "base", ""))
                            .append(" AND i18n.language = ?;");
                } else {
                    String sqlHeader = baseSql.substring(0, baseSql.indexOf("FROM"));
                    for (TableFieldInfo tableFieldInfo : tableFieldInfoList) {
                        if (i18nFieldList.contains(tableFieldInfo.getProperty())) {
                            // i18n的多语言field和base表的多语言field都拿出来
                            sqlHeader = replaceColumnWithTableAlias(sqlHeader,
                                    tableFieldInfo.getColumn().replaceAll("`", ""), "i18n",
                                    "base." + tableFieldInfo.getColumn() + " AS base_" + tableFieldInfo.getProperty()
                                            + ",");
                        } else {
                            sqlHeader = replaceColumnWithTableAlias(sqlHeader,
                                    tableFieldInfo.getColumn().replaceAll("`", ""), "base", "");
                        }
                        sqlWhere = replaceColumnWithTableAlias(sqlWhere.replaceAll("`", ""),
                                tableFieldInfo.getColumn().replaceAll("`", ""), "base", "");
                    }
                    // id特殊处理
                    sqlWhere = replaceColumnWithTableAlias(sqlWhere, "id", "base", "");
                    // 解决entityWrapper 特殊处理Where时的问题
                    if (!sqlWhere.contains("WHERE")) {
                        sqlWhere = sqlWhere.replaceFirst("AND", "WHERE");
                    }
                    sqlHeader = replaceColumnWithTableAlias(sqlHeader, "id", "base", "");
                    sb.append(sqlHeader).append(",i18n.language FROM ").append(tableInfo.getTableName())
                            .append(" base LEFT JOIN ").append(tableInfo.getTableName())
                            .append("_i18n i18n  ON base.id = i18n.id ").append(sqlWhere).append(";");
                }
                break;
            }
            case UPDATE: {
                sb.append(baseSql.substring(baseSql.indexOf("WHERE")));
                break;
            }
            case DELETE: {
                int offset = baseSql.indexOf("FROM");
                int firstBlankIndex = baseSql.indexOf(" ", offset);
                int secondBlankIndex = baseSql.indexOf(" ", firstBlankIndex + 1);
                String tableName = baseSql.substring(firstBlankIndex, secondBlankIndex).trim();
                sb.append(baseSql.substring(0, firstBlankIndex)).append(" ").append(tableName).append("_i18n  ")
                        .append("WHERE id IN (SELECT id FROM ").append(tableName)
                        .append(baseSql.substring(secondBlankIndex)).append(");");
                break;
            }
            default:
                throw new ServiceException("only support DELETE!");
        }
        return sb.toString();
    }

    /**
     * 针对column可能出现的真包含做的replace处理
     *
     * @param sqlStr 原始Sql
     * @param column column
     * @param tableAlias 表别名
     * @param additionStr 附加Str
     * @return
     */
    private String replaceColumnWithTableAlias(String sqlStr, String column, String tableAlias, String additionStr) {
        // sqlStr包含column
        if (sqlStr.contains(column)) {
            String regex = "\\b" + column + "\\b";
            sqlStr = sqlStr.replaceFirst(regex, additionStr + tableAlias + "." + column);
        }
        return sqlStr;
    }

    /**
     * 执行insertOrUpdateSql
     *
     * @param metaDataMap 整理过的metaData
     * @param baseTableName 基本表名称
     * @param connection 数据库连接
     * @param baseTableId 基本表id值
     */
    public void execInsertOrUpdateList(Map<String, Map<String, String>> metaDataMap, String baseTableName,
            Connection connection, Long baseTableId) {
        metaDataMap.forEach((k, v) -> execInsertOrUpdateOne(k, v, baseTableName, connection, baseTableId));
    }

    private void execInsertOrUpdateOne(String language, Map<String, String> fieldValueMap, String baseTableName,
            Connection connection, Long baseTableId) {
        String sqlOrigin = "INSERT INTO ${tableName}_i18n(id,language${fieldList}) VALUES(?,?${valueList}) ON DUPLICATE KEY UPDATE ${updateList};";
        Map<String, String> sqlParamMap = new HashMap<>();
        sqlParamMap.put("tableName", baseTableName);
        sqlParamMap.put("fieldList", "");
        sqlParamMap.put("valueList", "");
        sqlParamMap.put("updateList", "");
        int numberOfFields = fieldValueMap.size();
        int parameterIndex = 3;
        StringBuilder fieldListSb = new StringBuilder();
        StringBuilder valueListSb = new StringBuilder();
        StringBuilder updateListSb = new StringBuilder();
        Map<Integer, String> paramValueMap = new LinkedHashMap<>();
        for (String fieldName : fieldValueMap.keySet()) {
            String columnName = fieldName;
            if (StringUtils.containsUpperCase(fieldName)) {
                columnName = StringUtils.camelToUnderline(fieldName);
            }
            fieldListSb.append(",").append(columnName);
            valueListSb.append(",?");
            updateListSb.append(",").append(columnName).append("=?");
            paramValueMap.put(parameterIndex, fieldValueMap.get(fieldName));
            paramValueMap.put(parameterIndex + numberOfFields, fieldValueMap.get(fieldName));
            parameterIndex++;
        }
        sqlParamMap.put("fieldList", fieldListSb.toString());
        sqlParamMap.put("valueList", valueListSb.toString());
        sqlParamMap.put("updateList", updateListSb.substring(1));
        String sql = StrSubstitutor.replace(sqlOrigin, sqlParamMap);
        // output example:
        // sql = INSERT INTO art_department_i18n(id, language, name) VALUES(?,?,?) ON DUPLICATE KEY UPDATE name=?;
        try {
            PreparedStatement psm = connection.prepareStatement(sql);
            psm.setLong(1, baseTableId);
            psm.setString(2, language);
            for (Integer integer : paramValueMap.keySet()) {
                psm.setString(integer, paramValueMap.get(integer));
            }
            psm.execute();
            log.info("I18nSqlProcessInterceptor: {}", sql);
        } catch (SQLException e) {
            log.error("Error: Method execInsertOrUpdateOne execution error !", e);
        }
    }

    /**
     * 将原始metaData(Map<String,List<Map<String,String>>>)转换成Map<String, Map<String, String>>
     *
     * @param parameter 原始参数,包含metaData
     * @param i18nFieldNameList class含@I18nField注解的fieldNameList
     * @return k:language, v: k1:filed,v1:value
     */
    private Map<String, Map<String, String>> constructMetaDataMap(Object parameter, List<String> i18nFieldNameList) {
        Map<String, List<Map<String, String>>> metaData = ((BaseI18nEntity) parameter).getI18n();
        List<BaseI18nMetaData> baseI18nMetaDataList = new ArrayList<>();
        Map<String, Map<String, String>> metaDataMap = new HashMap<>();
        if (CollUtil.isEmpty(metaData)) {
            return metaDataMap;
        }
        metaData.forEach((k, v) -> {
            if (v == null) {
                throw new ServiceException("i18n参数未按照约定方式传值,请检查!");
            }
            v.forEach(p -> {
                // 暂不抛出异常,只处理被@I18nField注解的field的metaData
                if (i18nFieldNameList.contains(k)) {
                    baseI18nMetaDataList.add(new BaseI18nMetaData(k, p.get(LANGUAGE_CONSTANT), p.get(VALUE_CONSTANT)));
                }
            });
        });
        for (BaseI18nMetaData baseI18nMetaData : baseI18nMetaDataList) {
            String language = baseI18nMetaData.getLanguage();
            Map<String, String> subMap = new HashMap<>();
            if (metaDataMap.containsKey(language)) {
                subMap = metaDataMap.get(language);
            }
            subMap.put(baseI18nMetaData.getField(), baseI18nMetaData.getValue());
            metaDataMap.put(language, subMap);
        }
        // k:language, v: k1:filed,v1:value
        return metaDataMap;
    }

    /**
     * 方法是否支持
     *
     * @param sqlCommandType CRUD
     * @param methodStr methodNameStr
     * @return
     */
    private Boolean methodSupported(SqlCommandType sqlCommandType, String methodStr) {
        String[] methodArray = supportedOperationMap.get(sqlCommandType);
        if (methodArray == null) {
            return false;
        }
        return Arrays.asList(methodArray).contains(methodStr);
    }

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

    @Override
    public void setProperties(Properties properties) {
    }
}
