package com.sonic.daosupport.mysql;

import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

import org.apache.commons.lang3.tuple.Pair;
import org.springframework.util.CollectionUtils;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 一个简单工具类可以从测试环境中通过intellij工具导入数据成json字符串, 然后运行这个脚本导入到线上环境.
 * intellij中请使用JSON-groovy格式输出成json
 * 注意.导出的数据的字段是数据库的列名需要转换.
 * <pre>
 * [
 *      {
 *          "id": 2,
 *          "app_ids": "9999",
 *          "name": "集成测试告警",
 *          "subject": "集成测试告警",
 *          "priority": 98,
 *          "recipients": "bs-dev@gamers.gg",
 *          "send_threshold": 1,
 *          "send_interval": 1,
 *          "description": "集成测试告警",
 *          "ext": "",
 *          "status": "ENABLED",
 *          "create_at": "2019-10-25 07:10:43",
 *          "update_at": "2019-11-23 19:51:50"
 *      }
 *     ....
 * ]
 * </pre>
 * sample
 * <pre>
*         importHelper = JsonImportHelper.<AlertRuleDao, AlertRule>builder().baseMapper(alertRuleDao)
*                 .bizKeyNames(List.of("name"))
*                 .excludeFields(Set.of())
*                 .clz(AlertRule.class)
*                 .saveOrUpdate(null).build();
 * </pre>
 * @param <M>
 * @param <T>
 */
public class JsonImportHelper<M extends BaseMapper<T>, T> {

    private static final Set<String> DEFAULT_EXCLUDE_FIELDS = ImmutableSet.of("", "id", "updatedAt", "createdAt", "updateAt", "createAt");
    private static final String DEFAULT_ID_FIELD_NAME = "id";
    private static final int MAX_IMPORT_COUNT = 100;

    private String idFieldName;
    private HashSet<String> excludeFields;
    private M baseMapper;
    /**
     * 业务主键. 比如code, 唯一的名称...能唯一标识这条数据.
     */
    private List<String> bizKeyNames;
    private Class<T> clz;
    private BiConsumer<T, Boolean> saveOrUpdate;

    /**
     *
     * @param baseMapper
     * @param excludeFields 排除的字段. 比如更新日期,创建日期,id. 默认已经集成.
     * @param bizKeyNames
     * @param clz
     * @param saveOrUpdate
     */
    @Builder
    public JsonImportHelper(M baseMapper, Set<String> excludeFields, List<String> bizKeyNames, Class<T> clz,
                            BiConsumer<T, Boolean> saveOrUpdate, String idFieldName) {
        Preconditions.checkArgument(!CollectionUtils.isEmpty(bizKeyNames));
        Preconditions.checkArgument(baseMapper != null);
        Preconditions.checkArgument(clz != null);

        this.baseMapper = baseMapper;
        this.excludeFields = new HashSet<>(DEFAULT_EXCLUDE_FIELDS);
        if (!CollectionUtils.isEmpty(excludeFields)) {
            this.excludeFields.addAll(excludeFields);
        }
        this.bizKeyNames = bizKeyNames;
        this.clz = clz;
        this.saveOrUpdate = saveOrUpdate;
        this.idFieldName  = Strings.isNullOrEmpty(idFieldName) ? DEFAULT_ID_FIELD_NAME : idFieldName;
    }

    /**
     * 执行导入数据操作.返回需要更新, 查询, 没有变化的数据. 如果limit 是0 只是查看数据结果, 不发生操作.
     * 一次最多不超过200条数据
     * @param rawJson
     * @param limit 指定需要更新的数据数量. 如果是0, 不会更新数据. 只返回结果
     * @return 返回更新数据列表.
     */
    public ImportResp run(List<JSONObject> rawJson, int limit) {
        Preconditions.checkArgument(rawJson.size() <= MAX_IMPORT_COUNT);
        List<JSONObject> importRows = resolveRows(rawJson);
        QueryWrapper<T> query = new QueryWrapper<>();

        query.last("limit 1000");
        TableInfo tableInfo = TableInfoHelper.getTableInfo(clz);
        Map<String, String> columnMap = tableInfo.getFieldList().stream().collect(Collectors.toMap(e -> e.getColumn(), e -> e.getProperty()));
        // 主键没有columnMap中,需要显示声明.
        columnMap.put(tableInfo.getKeyColumn(), tableInfo.getKeyProperty());
        // 没有直接使用selectList处理json类型列有问题
        // baseMapper.selectMaps(query) 返回数据是db 列名, 需要转换
        List<JSONObject> dbRows = baseMapper.selectMaps(query).stream().map(e->{
            final Map<String, Object> p = e.entrySet().stream()
                    .collect(Collectors.toMap(entry -> columnMap.get(entry.getKey().toLowerCase()), entry -> entry.getValue()));
            return new JSONObject(p);
        }).collect(Collectors.toList());

        ImportResp<T> diffRes = diff(dbRows, importRows);
        diffRes.insertRows.stream().limit(limit).forEach(e -> doSaveOrUpdate(e, true));
        diffRes.updateRows.stream().limit(limit).forEach(e -> doSaveOrUpdate(e, false));
        return diffRes;
    }

    private void doSaveOrUpdate(T entity, boolean insert) {
        if (saveOrUpdate != null) {
            saveOrUpdate.accept(entity, insert);
        } else {
            if (insert) {
                baseMapper.insert(entity);
            } else {
                baseMapper.updateById(entity);
            }
        }
    }

    /**
     * 解决数据库的列名到属性名的转换, 并过滤不存在和需要过滤的列
     * @param rows
     * @return
     */
    List<JSONObject> resolveRows(List<JSONObject> rows) {
        TableInfo tableInfo = TableInfoHelper.getTableInfo(clz);
        // 兼容客户端上传的数据是表的column名称(下划线), 或者是熟悉的名称(camel)
        ImmutableMap<String, TableFieldInfo> columnMap = Maps.uniqueIndex(tableInfo.getFieldList(), TableFieldInfo::getColumn);
        ImmutableMap<String, TableFieldInfo> propertyMap = Maps.uniqueIndex(tableInfo.getFieldList(), TableFieldInfo::getProperty);
        Map<String, TableFieldInfo> columnPropertyMap = new HashMap<>(columnMap);
        columnPropertyMap.putAll(propertyMap);

        return rows.stream().map(e -> {
            Map<String, Object> collect = e.entrySet().stream().map(node -> {
                TableFieldInfo column = columnPropertyMap.get(node.getKey());
                if (column == null) {
                    return Pair.of("", "");
                }
                return Pair.of(column.getProperty(), node.getValue());
            }).filter(x -> !excludeFields.contains(x.getKey()))
                    .collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue()));
            return new JSONObject(collect);
        }).collect(Collectors.toList());
    }

    ImportResp<T> diff(List<JSONObject> dbRows, List<JSONObject> importRows) {
        // 通过逻辑biz key来关联数据库和导入的数据, 得到需要需要创建和更新的数据.
        ImmutableMap<String, JSONObject> dbRowMap = Maps.uniqueIndex(dbRows, e -> {
            return bizKeyNames.stream().map(key -> e.getString(key)).collect(Collectors.joining(","));
        });

        ImmutableMap<String, JSONObject> importRowMap = Maps.uniqueIndex(importRows, e -> {
            return bizKeyNames.stream().map(key -> e.getString(key)).collect(Collectors.joining(","));
        });
        ImportResp res = new ImportResp();
        for (Map.Entry<String, JSONObject> entry : importRowMap.entrySet()) {
            JSONObject dbRow = dbRowMap.get(entry.getKey());
            JSONObject importRow = entry.getValue();
            if (dbRow != null) {
                // 关联成功
                if (isSame(dbRow, importRow)) {
                    res.sameRows.add(JSONObject.toJavaObject(importRow, clz));
                } else {
                    // update the id by db row's
                    JSONObject updated = new JSONObject(importRow);
                    updated.put(this.idFieldName, dbRow.getLong(this.idFieldName));
                    res.updateRows.add(JSONObject.toJavaObject(updated, clz));
                }
            } else {
                // 没有关联上, 说明需要新增.
                res.insertRows.add(JSONObject.toJavaObject(importRow, clz));
            }
        }
        return res;
    }

    /**
     * 将json对象串成一个字段串, 来比较2个json是否一致.
     *
     * @param src
     * @param target
     * @return
     */
    boolean isSame(JSONObject src, JSONObject target) {
        Function<JSONObject, String> mixer = i -> {
            return i.entrySet().stream().filter(e -> !excludeFields.contains(e.getKey()))
                    .sorted(Comparator.comparing(Map.Entry::getKey))
                    .filter(e -> e.getValue() != null)
                    .filter(e -> !Strings.isNullOrEmpty(e.getValue().toString()))
                    .map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(","));
        };
        String srcText = mixer.apply(src);
        String targetText = mixer.apply(target);
        return srcText.equals(targetText);
    }

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    @Builder
    public final static class ImportResp<T> {
        List<T> updateRows = Lists.newArrayList();
        List<T> insertRows = Lists.newArrayList();
        List<T> sameRows = Lists.newArrayList();
    }
}
