package com.qmth.boot.data.upgrade.config;

import com.qmth.boot.data.upgrade.annotation.DataUpgradeVersion;
import com.qmth.boot.data.upgrade.model.BootAppInfo;
import com.qmth.boot.data.upgrade.model.RunMode;
import com.qmth.boot.data.upgrade.service.DataInitService;
import com.qmth.boot.data.upgrade.service.DataUpgradeService;
import com.qmth.boot.data.upgrade.utils.ClassHelper;
import com.qmth.boot.data.upgrade.utils.VersionComparator;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.ClassUtils;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 数据升级监听类
 *
 * @author: Deason
 * @since: 2025/3/3
 */
public class DataUpgradeListener implements ApplicationListener<ApplicationPreparedEvent>, DataUpgradeConstants {

    private static final Logger log = LoggerFactory.getLogger(DataUpgradeListener.class);

    /**
     * 数据库“应用信息表”表名
     */
    private static final String APP_INFO_TABLE = "boot_app_info";

    private DataUpgradeProperties properties;

    @Override
    public void onApplicationEvent(ApplicationPreparedEvent event) {
        try {
            properties = new DataUpgradeProperties(event.getApplicationContext().getEnvironment());

            if (RunMode.INSTALL == properties.getRunMode()) {
                // 首次安装模式，仅执行数据初始化
                this.handlerInstall();
                // 执行完后默认退出程序
                System.exit(1);
            }

            if (RunMode.UPGRADE == properties.getRunMode()) {
                // 升级模式，仅执行数据升级
                this.handlerUpgrade();
                // 执行完后默认退出程序
                System.exit(1);
            }

            // 启动模式
            this.handlerStart();
        } catch (Exception e) {
            log.error("程序异常终止！原因：{}", e.getMessage(), e);
            System.exit(1);
        }
    }

    public void handlerInstall() {
        try (HikariDataSource dataSource = this.initDataSource()) {
            JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

            log.warn("待初始化应用码:{} 待初始化应用版本号:{}", properties.getAppCode(), properties.getAppVersion());
            if (this.existBootAppInfo(jdbcTemplate)) {
                throw new RuntimeException("【应用信息表】已存在，不允许重复数据初始化！");
            }

            List<DataInitService> services = ClassHelper.findInterfaceInstances(DataInitService.class, properties.getBasePackage());
            if (services.isEmpty()) {
                log.warn("未找到数据初始化的具体实现！{}:{}", BASE_PACKAGE, properties.getBasePackage());
                throw new RuntimeException("未找到数据初始化的具体实现！");
            }

            if (services.size() > 1) {
                throw new RuntimeException("不允许存在多个数据初始化的具体实现！");
            }

            try {
                log.warn("*************** data init start ***************");
                services.get(0).process(jdbcTemplate);
            } catch (Exception e) {
                throw new RuntimeException("数据初始化执行异常！" + e.getMessage(), e);
            }

            // 数据初始化完成后，初始应用信息表
            this.initBootAppInfo(jdbcTemplate, properties.getAppCode(), properties.getAppVersion());
            log.warn("*************** data init finish ***************");
        }
    }

    public void handlerUpgrade() {
        try (HikariDataSource dataSource = this.initDataSource()) {
            JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

            if (!this.existBootAppInfo(jdbcTemplate)) {
                if (!properties.getBaseline()) {
                    throw new RuntimeException("【应用信息表】不存在！");
                }

                // 初始应用信息表和基线数据
                log.warn("初始基线应用码:{} 初始基线应用版本号:{}", properties.getBaselineAppCode(), properties.getBaselineAppVersion());
                this.initBootAppInfo(jdbcTemplate, properties.getBaselineAppCode(), properties.getBaselineAppVersion());
            }

            BootAppInfo appInfo = this.queryBootAppInfo(jdbcTemplate, properties.getAppCode());
            if (appInfo == null) {
                throw new RuntimeException("【应用信息表】版本数据未找到！appCode:" + properties.getAppCode());
            }

            log.warn("待升级应用码:{} 待升级应用版本号:{} 当前应用码:{} 当前应用版本号:{} locked:{}", properties.getAppCode(),
                    properties.getAppVersion(), appInfo.getAppCode(), appInfo.getAppVersion(), appInfo.getLocked());

            int compareValue = new VersionComparator().compare(appInfo.getAppVersion(), properties.getAppVersion());
            if (compareValue > 0) {
                throw new RuntimeException("待升级的版本号不能低于当前版本号！");
            }

            if (compareValue == 0) {
                log.warn("跳过数据升级，版本号一致！");
                return;
            }

            // 数据升级实现 按版本号由小到大排序
            Map<String, DataUpgradeService> serviceMaps = this.matchServices(appInfo.getAppVersion(), properties.getAppVersion());
            List<String> versionKeys = new ArrayList<>(serviceMaps.keySet());
            versionKeys.sort(new VersionComparator());

            // 升级锁定
            if (!tryLock(jdbcTemplate, appInfo.getAppCode())) {
                throw new RuntimeException("升级锁定中，请稍后再试！");
            }
            log.info("upgrade locking...");

            try {
                for (String version : versionKeys) {
                    log.warn("*************** data upgrade start version:{} ***************", version);
                    serviceMaps.get(version).process(jdbcTemplate);
                }
            } catch (Exception e) {
                throw new RuntimeException("数据升级执行异常！" + e.getMessage(), e);
            } finally {
                // 升级解锁
                unLock(jdbcTemplate, appInfo.getAppCode());
                log.info("upgrade unlock...");
            }

            // 数据升级完成后，更新应用信息表
            this.updateBootAppInfo(jdbcTemplate, properties.getAppCode(), properties.getAppVersion());
            log.warn("*************** data upgrade finish ***************");
        }
    }

    public void handlerStart() {
        try (HikariDataSource dataSource = this.initDataSource()) {
            JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

            if (!this.existBootAppInfo(jdbcTemplate)) {
                throw new RuntimeException("【应用信息表】不存在！");
            }

            BootAppInfo appInfo = this.queryBootAppInfo(jdbcTemplate, properties.getAppCode());
            if (appInfo == null) {
                throw new RuntimeException("【应用信息表】版本数据未找到！appCode:" + properties.getAppCode());
            }

            int compareValue = new VersionComparator().compare(appInfo.getAppVersion(), properties.getAppVersion());
            if (compareValue != 0) {
                log.warn("配置应用码:{} 配置应用版本号:{} 当前应用码:{} 当前应用版本号:{}", properties.getAppCode(),
                        properties.getAppVersion(), appInfo.getAppCode(), appInfo.getAppVersion());
                throw new RuntimeException("配置中的版本号与数据库中的应用版本号不一致！");
            }
        }
    }

    /**
     * 获取数据升级的具体实现集合
     */
    private Map<String, DataUpgradeService> matchServices(String currentVersion, String targetVersion) {
        boolean existTargetVersionImpl = false;
        VersionComparator vc = new VersionComparator();
        Map<String, DataUpgradeService> serviceMaps = new HashMap<>();

        List<Class<?>> implClasses = ClassHelper.findInterfaceImpls(DataUpgradeService.class, properties.getBasePackage());
        for (Class<?> implClass : implClasses) {
            DataUpgradeVersion annotation = implClass.getAnnotation(DataUpgradeVersion.class);
            if (annotation == null) {
                // 跳过未声明“数据升级版本”注解的实现
                continue;
            }

            String implVersion = annotation.value();
            if (!VersionComparator.isValidVersion(implVersion)) {
                throw new RuntimeException("实现类声明的版本号格式不正确！" + implClass.getSimpleName() + " @DataUpgradeVersion:" + implVersion);
            }

            int targetVersionCompare = vc.compare(implVersion, targetVersion);
            if (targetVersionCompare == 0) {
                existTargetVersionImpl = true;
            }

            if (vc.compare(implVersion, currentVersion) > 0 && targetVersionCompare <= 0) {
                log.debug("implClass:{} implVersion:{}", implClass.getSimpleName(), implVersion);
                if (serviceMaps.containsKey(implVersion)) {
                    throw new RuntimeException("同个版本数据升级的实现出现重复！版本:" + implVersion);
                }

                try {
                    Constructor<?> constructor = ClassUtils.getConstructorIfAvailable(implClass);
                    if (constructor != null) {
                        serviceMaps.put(implVersion, (DataUpgradeService) constructor.newInstance());
                    }
                } catch (Exception e) {
                    log.warn("{} class newInstance fail. {}", implClass.getSimpleName(), e.getMessage());
                }
            }
        }

        if (!existTargetVersionImpl) {
            log.warn("未找到数据升级的具体实现！版本:{} {}:{}", targetVersion, BASE_PACKAGE, properties.getBasePackage());
            throw new RuntimeException("未找到数据升级的具体实现！版本:" + targetVersion);
        }

        return serviceMaps;
    }

    private boolean tryLock(JdbcTemplate jdbcTemplate, String appCode) {
        try {
            return jdbcTemplate.update("update " + APP_INFO_TABLE + " set locked = 1 where app_code = ? and locked = 0", appCode) > 0;
        } catch (Exception e) {
            throw new RuntimeException("升级获取锁失败！appCode:" + appCode, e);
        }
    }

    private void unLock(JdbcTemplate jdbcTemplate, String appCode) {
        try {
            jdbcTemplate.update("update " + APP_INFO_TABLE + " set locked = 0 where app_code = ?", appCode);
        } catch (Exception e) {
            throw new RuntimeException("升级解锁失败！appCode:" + appCode, e);
        }
    }

    private void updateBootAppInfo(JdbcTemplate jdbcTemplate, String appCode, String appVersion) {
        try {
            jdbcTemplate.update("update " + APP_INFO_TABLE + " set app_version = ? where app_code = ?", appVersion, appCode);
        } catch (Exception e) {
            throw new RuntimeException("更新应用版本号失败！appCode:" + appCode, e);
        }
    }

    private BootAppInfo queryBootAppInfo(JdbcTemplate jdbcTemplate, String appCode) {
        try {
            List<BootAppInfo> list = jdbcTemplate.query("select app_code,app_version,locked from " + APP_INFO_TABLE + " where app_code = ?",
                    new String[]{appCode}, new BeanPropertyRowMapper<>(BootAppInfo.class));
            return list.isEmpty() ? null : list.get(0);
        } catch (Exception e) {
            throw new RuntimeException("获取应用信息失败！appCode:" + appCode, e);
        }
    }

    private void initBootAppInfo(JdbcTemplate jdbcTemplate, String appCode, String appVersion) {
        try {
            String createTableSql = "create table if not exists " + APP_INFO_TABLE + " (" +
                    " app_code varchar(20) not null," +
                    " app_version varchar(10) not null," +
                    " locked bit(1) not null default 0," +
                    " unique key(app_code)" +
                    ")";
            jdbcTemplate.execute(createTableSql);

            jdbcTemplate.update("insert into " + APP_INFO_TABLE + " (app_code,app_version) values(?,?)", appCode, appVersion);
        } catch (Exception e) {
            throw new RuntimeException("初始应用信息表失败！appCode:" + appCode, e);
        }
    }

    private boolean existBootAppInfo(JdbcTemplate jdbcTemplate) {
        try {
            // 是否存在应用信息表
            List<String> exists = jdbcTemplate.queryForList("SHOW TABLES LIKE '" + APP_INFO_TABLE + "'", String.class);
            return !exists.isEmpty();
        } catch (Exception e) {
            throw new RuntimeException("查询应用信息表失败！", e);
        }
    }

    private HikariDataSource initDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setPoolName("dataUpgradeDataSource");
        dataSource.setDriverClassName(properties.getDriverClassName());
        dataSource.setJdbcUrl(properties.getDbUrl());
        dataSource.setUsername(properties.getDbUsername());
        dataSource.setPassword(properties.getDbPassword());
        return dataSource;
    }

}
