package com.codvision.db.versionctl.task;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ClassLoaderUtil;
import com.codvision.db.enums.SysBusinessSpace;
import com.codvision.db.utils.ScriptReader;
import com.codvision.db.versionctl.chain.DbVersionCtlAbstractTask;
import com.codvision.db.versionctl.chain.DbVersionCtlContext;
import com.codvision.db.versionctl.entity.DbVersionEntity;
import com.codvision.db.versionctl.entity.SQLScriptEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import java.io.*;
import java.sql.SQLException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 任务:查找并执行增量SQL并记录数据库版本
 *
 * @author lingee
 */
@Slf4j
@SuppressWarnings("unused")
public class IncreaseVersionTask extends DbVersionCtlAbstractTask {

    public IncreaseVersionTask(DbVersionCtlContext context) {
        super(context);
    }

    @Override
    public boolean runTask() {
        logger.info("IncreaseVersionTask begin...");

        // 生成数据库版本插入SQL语句
        String insertSql = this.context.makeInsertSql();

        // 获取数据库版本升级SQL脚本目录集合
        List<String> scriptDirPaths = this.context.getDbVersionCtlProps().makeScriptDirPaths();
        logger.debug("IncreaseVersionTask scriptDirPaths:{}", scriptDirPaths);

        // 生成sql脚本对象集合
        List<SQLScriptEntity> sqlByBs = createSqlScriptEntities(scriptDirPaths);

        // 执行脚本升级数据库版本
        increaseDbVersion(insertSql, sqlByBs);

        // 关闭所有输入流
        sqlByBs.forEach(SQLScriptEntity::closeInputStream);

        logger.info("IncreaseVersionTask end...");
        return true;
    }

    private void increaseDbVersion(String insertSql, List<SQLScriptEntity> sqlByBs) {
        String updateSql = this.context.makeUpdateSql();

        Map<String, List<SQLScriptEntity>> sqlGrpByBs = sqlByBs.stream()
                .collect(Collectors.groupingBy(SQLScriptEntity::getBusinessSpace));

        /* 系统相关服务数据库脚本需要按顺序执行 */
        LinkedHashMap<String, List<SQLScriptEntity>> sqlGrpByBsLinked = new LinkedHashMap<>();
        for (SysBusinessSpace sys : SysBusinessSpace.values()) {
            if (CollectionUtil.isNotEmpty(sqlGrpByBs.get(sys.getCode()))) {
                sqlGrpByBsLinked.put(sys.getCode(), sqlGrpByBs.get(sys.getCode()));
                sqlGrpByBs.remove(sys.getCode());
            }
        }
        if (CollectionUtil.isNotEmpty(sqlGrpByBs)) {
            sqlGrpByBsLinked.putAll(sqlGrpByBs);
        }

        for (Map.Entry<String, List<SQLScriptEntity>> gp : sqlGrpByBsLinked.entrySet()) {
            List<DbVersionEntity> dbVersionEntities = queryDbVersionEntities(gp.getKey());

            List<SQLScriptEntity> sqlScriptEntities = gp.getValue();
            if (!dbVersionEntities.isEmpty()) {
                for (DbVersionEntity dbVersionEntity : dbVersionEntities) {
                    int curMajor = dbVersionEntity.getMajorVersion();
                    int curMinor = dbVersionEntity.getMinorVersion();
                    int curPatch = dbVersionEntity.getPatchVersion();
                    int curExtend = dbVersionEntity.getExtendVersion();
                    String versionType = dbVersionEntity.getVersionType();
                    String curHex = dbVersionEntity.getScriptDigestHex();

                    sqlScriptEntities = sqlScriptEntities.stream()
                            .filter(sqlScriptEntity -> sqlScriptEntity.checkNeed(curMajor, curMinor, curPatch, curExtend, versionType, curHex))
                            .collect(Collectors.toList());
                }
            }

            if (sqlScriptEntities.isEmpty()) {
                logger.info("业务空间 {} 没有增量sql脚本需要执行.", gp.getKey());
                continue;
            }
            // 按版本排序
            CollectionUtil.sort(sqlScriptEntities, SQLScriptEntity::compareTo);

            sqlScriptEntities.forEach(sqlScriptEntity -> {
                logger.info("增量执行脚本:{}", sqlScriptEntity.getFileName());

                // 读取脚本内容
                ScriptReader scriptReader = new ScriptReader(sqlScriptEntity.getInputStream());
                List<String> sqls = scriptReader.readSqls();

                // 开始时间
                LocalDateTime startTime = LocalDateTime.now();

                // 插入版本记录
                this.jdbcUtil.execute(this.connection, insertSql,
                        sqlScriptEntity.getBusinessSpace(), sqlScriptEntity.getMajorVersion(),
                        sqlScriptEntity.getMinorVersion(), sqlScriptEntity.getPatchVersion(), sqlScriptEntity.getExtendVersion(),
                        sqlScriptEntity.getVersion(), sqlScriptEntity.getCustomName(),
                        "SQL", sqlScriptEntity.getFileName(), sqlScriptEntity.getHex(), 0, -1,
                        DateTimeFormatter.ofPattern(DbVersionCtlContext.DATETIME_PTN).format(startTime),
                        this.context.getDbVersionCtlProps().getUsername());

                // 执行脚本
                this.jdbcUtil.executeWithConnection(this.connection, sqls);
                LocalDateTime stopTime = LocalDateTime.now();
                long mills = Duration.between(startTime, stopTime).toMillis();
                logger.info("sql脚本 {} 执行耗时 : {} ms.", sqlScriptEntity.getFileName(), mills);

                // 更新版本记录
                this.jdbcUtil.execute(this.connection, updateSql,
                        mills, sqlScriptEntity.getBusinessSpace(),
                        sqlScriptEntity.getMajorVersion(), sqlScriptEntity.getMinorVersion(),
                        sqlScriptEntity.getPatchVersion(), sqlScriptEntity.getExtendVersion());
                logger.info("数据库版本记录更新, business_space: {} , major_version: {} , minor_version: {} , patch_version: {} , extend_version: {} .",
                        sqlScriptEntity.getBusinessSpace(), sqlScriptEntity.getMajorVersion(),
                        sqlScriptEntity.getMinorVersion(), sqlScriptEntity.getPatchVersion(), sqlScriptEntity.getExtendVersion());
            });
        }
    }

    private List<SQLScriptEntity> createSqlScriptEntities(List<String> scriptDirPaths) {
        List<SQLScriptEntity> sqlByBs = new ArrayList<>();
        try {
            for (String scriptDirPath : scriptDirPaths) {
                if (scriptDirPath.startsWith("classpath")) {
                    readSqlFromClassPath(sqlByBs, scriptDirPath);
                } else {
                    readSqlFromFile(sqlByBs, scriptDirPath);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return sqlByBs;
    }

    private void readSqlFromClassPath(List<SQLScriptEntity> sqlByBs, String scriptDirPath) throws IOException {
        String classPath = scriptDirPath.substring(scriptDirPath.indexOf(":") + 1);
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        String pathPattern = scriptDirPath.endsWith("/") ? scriptDirPath + "*.sql" : scriptDirPath + "/*.sql";
        Resource[] resources = resolver.getResources(pathPattern);
        for (Resource resource : resources) {
            if (!resource.getURL().getPath().startsWith("file:")) {
                Resource resourcePath = new ClassPathResource(classPath + resource.getFilename());
                sqlByBs.add(new SQLScriptEntity(resourcePath.getFilename(), resourcePath.getInputStream()));
            } else {
                InputStream inputStream = ClassLoaderUtil.getClassLoader().getResourceAsStream(classPath + resource.getFilename());
                sqlByBs.add(new SQLScriptEntity(resource.getFilename(), inputStream));
            }
        }
    }

    private void readSqlFromFile(List<SQLScriptEntity> sqlByBs, String scriptDirPath) throws FileNotFoundException {
        File folder = new File(scriptDirPath);
        if (folder.exists() && folder.isDirectory()) {
            File[] files = folder.listFiles((dir, name) -> name.endsWith(".sql"));
            if (files == null || files.length == 0) {
                throw new RuntimeException("There is no sql files in [" + scriptDirPath + "]!");
            }
            for (File file : files) {
                sqlByBs.add(new SQLScriptEntity(file.getName(), new FileInputStream(file)));
            }
        } else {
            throw new RuntimeException(scriptDirPath + " is not Filesystem Directory!");
        }
    }

    private List<DbVersionEntity> queryDbVersionEntities(String bs) {
        String selectSql = this.context.makeSelectSql();

        List<DbVersionEntity> dbVersionEntities = new ArrayList<>();
        try {
            this.jdbcUtil.getRunner().query(this.connection, selectSql, resultSet -> {
                while (resultSet.next()) {
                    DbVersionEntity dbVersionEntity = new DbVersionEntity();
                    dbVersionEntity.setId(resultSet.getInt("id"));
                    dbVersionEntity.setBusinessSpace(resultSet.getString("business_space"));
                    dbVersionEntity.setMajorVersion(resultSet.getInt("major_version"));
                    dbVersionEntity.setMinorVersion(resultSet.getInt("minor_version"));
                    dbVersionEntity.setPatchVersion(resultSet.getInt("patch_version"));
                    dbVersionEntity.setExtendVersion(resultSet.getInt("extend_version"));
                    dbVersionEntity.setVersion(resultSet.getString("version"));
                    dbVersionEntity.setCustomName(resultSet.getString("version"));
                    dbVersionEntity.setVersionType(resultSet.getString("version_type"));
                    dbVersionEntity.setScriptFileName(resultSet.getString("script_file_name"));
                    dbVersionEntity.setScriptDigestHex(resultSet.getString("script_digest_hex"));
                    dbVersionEntity.setSuccess(resultSet.getByte("success"));
                    dbVersionEntity.setExecutionTime(resultSet.getInt("execution_time"));
                    dbVersionEntity.setInstallTime(resultSet.getString("install_time"));
                    dbVersionEntity.setInstallUser(resultSet.getString("install_user"));
                    dbVersionEntities.add(dbVersionEntity);
                }
                return null;
            }, bs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return dbVersionEntities;
    }
}
