package com.qiaoba.common.database.monitor;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import com.qiaoba.common.base.constant.TenantConstant;
import com.qiaoba.common.base.enums.DataBaseEnum;
import com.qiaoba.common.database.config.DynamicDataSourceConfig;
import com.qiaoba.common.database.context.BackupDatasourceContext;
import com.qiaoba.common.database.context.PrimaryDatasourceContext;
import com.qiaoba.common.database.entity.DynamicDataSource;
import com.qiaoba.common.database.service.DynamicDatasourceService;
import com.qiaoba.common.database.util.DatasourceUtil;
import com.qiaoba.common.database.util.JdbcUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 在线的数据源监控
 * 尝试连接, 如果连接不成功, 替换可用数据源, 将失败数据源加入到错误数据源, 等待重试
 *
 * @author ailanyin
 * @version 1.0
 * @since 2023/6/26 10:46
 */
@Component
@Slf4j
public class OnlineDatasourceMonitor {


    private static final String LOCK_KEY = "lock4j:onlineDatasourceMonitor";
    private static Map<String, String> WAIT_UPDATE_DATASOURCE_STATUS = new ConcurrentHashMap<>();
    private static Map<String, List<String>> WAIT_ADD_ERROR_MAP = new ConcurrentHashMap<>();

    @Resource
    private LockTemplate lockTemplate;
    @Resource
    private DynamicDatasourceService dynamicDatasourceService;

    @PostConstruct
    public void init() {
        // 1s 运行一次
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                // 项目启动时加载数据源还未完成
                if (!DynamicDataSourceConfig.COMPLETE_LOAD_DATASOURCE) {
                    return;
                }

                // expire = -1 锁自动续期, 防止数据源过多或异常等待, 超过默认锁 30s
                final LockInfo lockInfo = lockTemplate.lock(LOCK_KEY, -1, 1000);
                //申请锁失败 说明集群中其他设备正在执行监控
                if (null == lockInfo) {
                    return;
                }
                //申请锁成功
                try {
                    log.trace("开始运行数据源监控线程, 时间: {}", new Date());
                    tryConnect();
                    log.trace("结束运行数据源监控线程, 时间: {}", new Date());
                } finally {
                    // 释放锁
                    lockTemplate.releaseLock(lockInfo);
                }

            }
        }, 0, 1000);
    }

    private void tryConnect() {
        for (String tenantId : DynamicDataSourceConfig.TENANT_IDS) {
            Object primary = PrimaryDatasourceContext.get(tenantId);
            if (Objects.isNull(primary)) {
                // 说明初始化主要数据源的时候出错
                log.error("租户[{}]-目前主数据源异常, 开始切换备用数据源", tenantId);
                // 切换备用数据源为主数据源
                backToPrimary(tenantId);
                continue;
            }

            DruidDataSource dataSource = (DruidDataSource) primary;
            Connection connection = null;
            try {
                connection = dataSource.getConnection();
                if (JdbcUtil.checkConnect(connection)) {
                    // 说明数据源正常
                    log.trace("租户[{}]-目前主数据源正常, 无需切换数据源", tenantId);
                    // 系统默认主数据源 处理任务
                    if (TenantConstant.DEFAULT_TENANT_ID.equals(tenantId)) {
                        handleUpdateDatasourceStatus();
                        handleErrorDatasource();
                    }
                    continue;
                }

                log.error("租户[{}]-目前主数据源异常, 开始切换备用数据源", tenantId);
                // 主数据源异常 切换备用数据源
                if (!backToPrimary(tenantId)) {
                    // 备用切换失败
                    // 关闭原有异常数据源
                    IoUtil.close(dataSource);
                    // 在数据源Map中删除
                    PrimaryDatasourceContext.remove(tenantId);
                }
                // 将原有异常数据源加入到错误数据源Map, 等待重试
                addErrorDatasource(tenantId, dataSource);

            } catch (SQLException e) {

            } finally {
                IoUtil.close(connection);
            }
        }
    }

    /**
     * 切换备用数据源为主数据源
     *
     * @param tenantId 租户ID
     */
    private Boolean backToPrimary(String tenantId) {
        // 备用数据源
        List<DynamicDataSource> dataSources = BackupDatasourceContext.get(tenantId);
        if (CollUtil.isEmpty(dataSources)) {
            log.error("租户:[{}]切换备用数据源失败, 原因: 没有备用数据源", tenantId);
            return false;
        }

        Integer backupIndex = null;

        for (int i = 0; i < dataSources.size(); i++) {
            Object dynamicDataSource = DatasourceUtil.buildDataSource(tenantId, dataSources.get(i));
            // 不是空,说明备用数据源有用
            if (Objects.nonNull(dynamicDataSource)) {
                DatasourceUtil.changePrimaryDatasource(tenantId, dynamicDataSource);
                backupIndex = i;
                break;
            }
        }

        if (Objects.nonNull(backupIndex)) {
            // 切换成功
            DynamicDataSource dynamicDataSource = dataSources.get(backupIndex);
            // 更改数据库中该数据源为主要数据源
            if (Objects.nonNull(dynamicDataSource.getTenantId()) && !TenantConstant.DEFAULT_TENANT_ID.equals(dynamicDataSource.getTenantId())) {
                // 添加到待处理任务中
                WAIT_UPDATE_DATASOURCE_STATUS.put(dynamicDataSource.getTenantId(), dynamicDataSource.getDatasourceId());
            }
            // 备用数据源集合删除该数据源
            dataSources.remove((int) backupIndex);
            log.info("租户:[{}]切换备用数据源成功, 现主数据ID: {}", tenantId, dynamicDataSource.getDatasourceId());
            return true;
        } else {
            log.error("租户:[{}]切换备用数据源失败, 原因: 备用数据源均无效", tenantId);
            return false;
        }
    }

    private void handleUpdateDatasourceStatus() {
        Set<String> keys = WAIT_UPDATE_DATASOURCE_STATUS.keySet();
        for (String key : keys) {
            try {
                log.info("开始更新数据库中租户数据源状态, 租户ID: {}", key);
                // 防止更新过程中主数据挂了
                dynamicDatasourceService.changePrimaryDatasource(key, WAIT_UPDATE_DATASOURCE_STATUS.get(key));
                // 处理完成 删除任务
                WAIT_UPDATE_DATASOURCE_STATUS.remove(key);
                log.info("更新数据库中租户数据源状态完成, 租户ID: {}", key);
            } catch (Exception e) {
                log.error("更新数据库中租户数据源状态未完成, 租户ID: {}, 失败原因: {}", key, e.getMessage());
            }
        }
    }

    private void handleErrorDatasource() {
        Set<String> tenantIds = WAIT_ADD_ERROR_MAP.keySet();
        for (String tenantId : tenantIds) {
            List<String> ipList = WAIT_ADD_ERROR_MAP.get(tenantId);
            for (String ip : ipList) {
                NotOnlineDatasourceMonitor.addErrorDatasource(tenantId, dynamicDatasourceService.selectByIp(tenantId, ip));
            }
            WAIT_ADD_ERROR_MAP.remove(tenantId);
        }
    }

    private void addErrorDatasource(String tenantId, DruidDataSource dataSource) {
        // 主系统
        if (TenantConstant.DEFAULT_TENANT_ID.equals(tenantId)) {
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            dynamicDataSource.setPassword(dataSource.getPassword());
            dynamicDataSource.setUsername(dataSource.getUsername());
            dynamicDataSource.setUrl(dataSource.getUrl());
            dynamicDataSource.setDriver(dataSource.getDriverClassName());
            dynamicDataSource.setTenantId(tenantId);
            dynamicDataSource.setInitialSize(dataSource.getInitialSize());
            dynamicDataSource.setMaxActive(dataSource.getMaxActive());
            dynamicDataSource.setMinIdle(dataSource.getMinIdle());
            NotOnlineDatasourceMonitor.addErrorDatasource(tenantId, dynamicDataSource);
            return;
        }


        // 普通租户
        String ip = DataBaseEnum.getIp(dataSource.getUrl(), dataSource.getDriverClassName());
        List<String> errorIpList = WAIT_ADD_ERROR_MAP.get(tenantId);
        if (CollUtil.isEmpty(errorIpList)) {
            WAIT_ADD_ERROR_MAP.put(tenantId, CollUtil.toList(ip));
        } else {
            errorIpList.add(ip);
        }
    }
}
