package com.xisc.pm.infra.util;

import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.mapping.SqlCommandType;
import org.hzero.mybatis.helper.AutoFillHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import cn.hutool.core.util.IdUtil;
import io.choerodon.core.oauth.CustomUserDetails;
import io.choerodon.core.oauth.DetailsHelper;
import io.choerodon.mybatis.domain.AuditDomain;
import io.choerodon.mybatis.domain.EntityColumn;
import io.choerodon.mybatis.domain.EntityTable;
import io.choerodon.mybatis.helper.EntityHelper;
import io.choerodon.mybatis.helper.MapperHelper;
import io.choerodon.mybatis.helper.SqlHelper;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class BatchInsertUtils {

    @Autowired
    private MapperHelper mapperHelper;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public <T> void execBatchInsert(List<T> data, Class<T> entityClass, String tableName, long batchSize) {
        long start = System.currentTimeMillis();
        int allCount = data.size();
        long limit = (allCount + batchSize - 1) / batchSize;
        Stream.iterate(0, n -> n + 1).limit(limit).forEach(i -> {
            List<T> subList = data.stream().skip(i * batchSize)
                    .limit(batchSize).collect(Collectors.toList());
            if(subList != null && !subList.isEmpty()) {
                long startSql = System.currentTimeMillis();
                String sql = sqlScript(subList, entityClass, tableName);
                long endSql = System.currentTimeMillis() - startSql;
                jdbcTemplate.batchUpdate(sql);
                log.info("生成sql耗时："+ endSql +" 当前批次执行完成：插入" + tableName + " 表 "+ subList.size() + "条，执行耗时："+  (System.currentTimeMillis() - start));
            }
        });
    }
    
    /**
     * 批量插入-支持seata
     * insert all
     *  into tableName() values()
     * select * from dual
     * @param data
     * @param batchSize
     */
    @Transactional(rollbackFor = Exception.class)
    public <T> void batchInsert(List<T> data, long batchSize) {
        if(data == null || data.isEmpty()) {
            return ;
        }
        @SuppressWarnings("unchecked")
        Class<T> clazz = ( Class<T>)data.get(0).getClass();
        EntityTable entityTable = EntityHelper.getEntityTable(clazz);
        long start = System.currentTimeMillis();
        int allCount = data.size();
        long limit = (allCount + batchSize - 1) / batchSize;
        Stream.iterate(0, n -> n + 1).limit(limit).forEach(i -> {
            List<T> subList = data.stream().skip(i * batchSize)
                    .limit(batchSize).collect(Collectors.toList());
            if(subList != null && !subList.isEmpty()) {
                long startSql = System.currentTimeMillis();
                String sql = sqlScript2(subList, clazz, entityTable.getName() , entityTable);
                long endSql = System.currentTimeMillis() - startSql;
                jdbcTemplate.batchUpdate(sql);
                log.info("生成sql耗时："+ endSql +" 当前批次执行完成：插入" + entityTable.getName() + " 表 "+ subList.size() + "条，执行耗时："+  (System.currentTimeMillis() - start));
            }
        });
    }
    
    /**
     * 批量插入，支持Seata
     * @param data
     * @param entityClass
     * @param tableName
     * @param batchSize
     */
    @Transactional(rollbackFor = Exception.class)
    public <T> void batchInsert(List<T> data, Class<T> entityClass, String tableName, long batchSize) {
        EntityTable entityTable = EntityHelper.getEntityTable(entityClass);
        long start = System.currentTimeMillis();
        int allCount = data.size();
        long limit = (allCount + batchSize - 1) / batchSize;
        Stream.iterate(0, n -> n + 1).limit(limit).forEach(i -> {
            List<T> subList = data.stream().skip(i * batchSize)
                    .limit(batchSize).collect(Collectors.toList());
            if(subList != null && !subList.isEmpty()) {
                long startSql = System.currentTimeMillis();
                String sql = sqlScript2(subList, entityClass, tableName , entityTable);
                long endSql = System.currentTimeMillis() - startSql;
                jdbcTemplate.batchUpdate(sql);
                log.info("生成sql耗时："+ endSql +" 当前批次执行完成：插入" + tableName + " 表 "+ subList.size() + "条，执行耗时："+  (System.currentTimeMillis() - start));
            }
        });
    }
    
    public <T> String sqlScript(List<T> data, Class<T> entityClass, String tableName) {
        StringBuilder sql = new StringBuilder();
        CustomUserDetails customUserDetails = DetailsHelper.getUserDetails();
        EntityTable entityTable = EntityHelper.getEntityTable(entityClass);
        boolean publishEnable = this.mapperHelper.getConfig().isPublishEnable()
                && (entityTable.getPublishStatusColumn() != null
                        || !CollectionUtils.isEmpty(entityTable.getPublishIgnoreList()));

        sql.append(SqlHelper.insertIntoTable(entityClass, tableName));
        Set<EntityColumn> columnList = EntityHelper.getColumns(entityClass);
        sql.append(" ( ");
        this.insertColumns(sql, columnList, true, publishEnable , customUserDetails);
        sql.append(") SELECT ");
        this.insertColumns(sql, columnList, false, publishEnable , customUserDetails);
        sql.append(" FROM (");
        List<Long> ids = getNextKeys(tableName + "_S" , data.size());
        int i = 0 ;
            
        for (T obj : data) {
            Long id = (ids == null || ids.isEmpty()) ? IdUtil.getSnowflakeNextId() : ids.get(i);
            i++;
            sql.append(" SELECT  ");
            Iterator<?> var10 = columnList.iterator();
            EntityColumn column;
            boolean firstFlag = true ;
            p: do {
                do {
                    if (!var10.hasNext()) {
                        
                        sql.append(" FROM DUAL ");
                        break p;
                    }
                    column = (EntityColumn) var10.next();
                    Boolean result = this.insertColumnValue(customUserDetails , sql, column, ObjectToMapUtil.beanToMap(obj), id , firstFlag);
                    if(result != null) {
                        firstFlag = !result;
                    }
                } while (!publishEnable);
            } while (!column.isPublishStatus() && !column.isPublishIgnore());

            if (i != data.size()) {
                sql.append(" UNION ALL  ");
            } else {
                sql.append(" )  ");
            }
        }
        return sql.toString();
    }
    
    
    public <T> String sqlScript2(List<T> data, Class<T> entityClass, String tableName , EntityTable entityTable) {
        CustomUserDetails customUserDetails = DetailsHelper.getUserDetails();
        StringBuilder sql = new StringBuilder();
        boolean publishEnable = this.mapperHelper.getConfig().isPublishEnable()
                && (entityTable.getPublishStatusColumn() != null
                        || !CollectionUtils.isEmpty(entityTable.getPublishIgnoreList()));
        sql.append("insert all ");
        Set<EntityColumn> columnList = EntityHelper.getColumns(entityClass);
        StringBuilder col = new StringBuilder();
        this.insertColumns(col, columnList, true, publishEnable , customUserDetails);
        String insertSql = " into "+ tableName + " ( " + col.toString() + ") values ( ";
        List<Long> ids = getNextKeys(tableName + "_S" , data.size());
        int i = 0 ;
        for (T obj : data) {
            Long id = (ids == null || ids.isEmpty()) ? IdUtil.getSnowflakeNextId() : ids.get(i++);
            sql.append(insertSql);
            Iterator<?> var10 = columnList.iterator();
            EntityColumn column;
            boolean firstFlag = true ;
            p: do {
                do {
                    if (!var10.hasNext()) {
                        sql.append(")");
                        break p;
                    }
                    column = (EntityColumn) var10.next();
                    Boolean result = this.insertColumnValue(customUserDetails ,sql, column, ObjectToMapUtil.beanToMap(obj), id , firstFlag , false );
                    if(result != null) {
                        firstFlag = !result;
                    }
                } while (!publishEnable);
            } while (!column.isPublishStatus() && !column.isPublishIgnore());
        }
        sql.append(" select * from dual");
        return sql.toString();
    }
    
    private List<Long> getNextKeys(String seqName, int count) throws DataAccessException {
        if (StringUtils.isEmpty(seqName)) {
            throw new DataAccessResourceFailureException("sequence name must be set.");
        }

        String sql = "select " + seqName + ".nextval from dual connect by rownum <= " + count;
        return jdbcTemplate.query(sql, rs -> {
            if (!rs.next()) {
                throw new DataAccessResourceFailureException("Get sequence name failed after executing an update");
            }

            List<Long> result = new ArrayList<>();

            // 先添加第一个
            result.add(rs.getLong(1));

            // 之后添加后面的
            while (rs.next()) {
                result.add(rs.getLong(1));
            }

            return result;
        });
    }
    
    /**
     * 插入列
     * 
     * @param sql
     * @param columnList
     * @param withTrim
     * @param publishEnable
     * @param customUserDetails 
     */
    protected void insertColumns(StringBuilder sql, Set<EntityColumn> columnList, boolean withTrim,
            boolean publishEnable, CustomUserDetails customUserDetails) {
        List<String> flag = new ArrayList<String>();
        columnList.forEach((column) -> {
            if (column.isInsertable()) {
                String columnName = column.getProperty();
                if (column.isInsertable()) {
                    if (AutoFillHelper.isAutoFill(column)) {
                        if (!AutoFillHelper.isIgnore(column, SqlCommandType.INSERT)) {
                            if(customUserDetails != null && (column.getProperty().equals(AuditDomain.FIELD_CREATED_BY) 
                                    || column.getProperty().equals(AuditDomain.FIELD_LAST_UPDATED_BY))) {
                                if(flag.size() == 0) {
                                    sql.append(column.getColumn());
                                    flag.add("1");
                                } else {
                                    sql.append(",").append(column.getColumn());
                                }
                            }
                        }

                    } else {
                        if (!SqlHelper.MODIFY_AUDIT_FIELDS.contains(columnName)
                                && !SqlHelper.VERSION_AUDIT_FIELDS.contains(columnName)) {
                            if(flag.size() == 0) {
                                sql.append(column.getColumn());
                                flag.add("1");
                            } else {
                                sql.append(",").append(column.getColumn());
                            }
                            
                        }
                    }
                }
            }
        });
    }
    
    private String convertValueByColumn(Object obj, EntityColumn column) {
        if(column.getJavaType().getName().equals(BigDecimal.class.getName())) {
            return obj == null ? "null" : ((BigDecimal) obj).toPlainString();
        } else if(column.getJavaType().getName().equals(Long.class.getName())) {
            return obj == null ? "null" : obj.toString();
        } else if(column.getJavaType().getName().equals(Integer.class.getName())) {
            return obj == null ? "null" : obj.toString();
        } else if(column.getJavaType().getName().equals(Date.class.getName())) {
            if(obj == null) {
                return "null";
            } else {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                return "TO_DATE('" + sdf.format((Date) obj) + "', 'yyyy-mm-dd hh24:mi:ss')";
            }
        } else if(column.getJavaType().getName().equals(String.class.getName())) {
            return obj == null ? "null" : "'" + obj.toString() + "'";
        }
        return "'" + obj.toString() + "'";
    }

    private String convertValue(Object obj, EntityColumn column) {
        if (obj == null) {
            return "''";
        }
        if (obj instanceof BigDecimal) {
            return ((BigDecimal) obj).toPlainString();
        }
        if (obj instanceof Long) {
            return obj.toString();
        }
        if (obj instanceof Integer) {
            return obj.toString();
        }
        if (obj instanceof Date) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            return "'" + sdf.format((Date) obj) + "'";
        }
        return "'" + obj.toString() + "'";
    }
    
    protected Boolean insertColumnValue(CustomUserDetails customUserDetails ,StringBuilder sql, EntityColumn column, Map<String, Object> objMap, Long id , boolean fisrtFlag) {
        return insertColumnValue(customUserDetails ,sql, column, objMap, id, fisrtFlag , true);
    }

    protected Boolean insertColumnValue(CustomUserDetails customUserDetails ,StringBuilder sql, EntityColumn column, Map<String, Object> objMap, Long id , boolean fisrtFlag , boolean needSeparator) {
        if (column.isInsertable()) {
            String columnName = column.getProperty();
            String separator = "";
            if(needSeparator) {
                separator = " as " + column.getColumn();
            }
            if (column.isInsertable()) {
                if (AutoFillHelper.isAutoFill(column)) {
                    if(customUserDetails != null && (column.getProperty().equals(AuditDomain.FIELD_CREATED_BY) 
                            || column.getProperty().equals(AuditDomain.FIELD_LAST_UPDATED_BY))) {
                        if(!fisrtFlag) {
                            sql.append(",");
                        }
                        if(needSeparator) {
                            sql.append(convertValue(customUserDetails.getUserId() , column) + separator);
                        } else {
                            sql.append(convertValueByColumn(customUserDetails.getUserId() , column) + separator);
                        }
                        return true ;
                    }
                } else {
                    if (column.isInsertable() && !SqlHelper.MODIFY_AUDIT_FIELDS.contains(columnName)
                            && !SqlHelper.VERSION_AUDIT_FIELDS.contains(columnName)) {
                        if (column.isIdentity()) {
                            if(!fisrtFlag) {
                                sql.append(",");
                            }
                            if(id != null) {
                                sql.append(id.toString() + separator);
                            } else {
                                sql.append(IdUtil.getSnowflakeNextId() + separator);
                            }
                            return true ;
                        } else {
                            if(!fisrtFlag) {
                                sql.append(",");
                            }
                            if(needSeparator) {
                                sql.append(convertValue(objMap.get(columnName) , column) + separator);
                            } else {
                                sql.append(convertValueByColumn(objMap.get(columnName) , column) + separator);
                            }
                            return true ;
                        }
                    } else if (SqlHelper.VERSION_AUDIT_FIELDS.contains(columnName)) {
                        if(!fisrtFlag) {
                            sql.append(",");
                        }
                        sql.append("1" + separator);
                        return true ;
                    }
                }
            }
        }
        return null;
    }

}
