/*
 * Copyright 1999-2023 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.nacos.persistence.datasource;

import com.alibaba.nacos.common.utils.ConvertUtils;
import com.alibaba.nacos.common.utils.InternetAddressUtil;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.persistence.configuration.DatasourceConfiguration;
import com.alibaba.nacos.persistence.monitor.DatasourceMetrics;
import com.alibaba.nacos.persistence.utils.ConnectionCheckUtil;
import com.alibaba.nacos.persistence.utils.DatasourcePlatformUtil;
import com.alibaba.nacos.persistence.utils.PersistenceExecutor;
import com.alibaba.nacos.sys.env.EnvUtil;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;

import javax.sql.DataSource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * Base data source.
 *
 * @author Nacos
 */
public class ExternalDataSourceServiceImpl implements DataSourceService {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(ExternalDataSourceServiceImpl.class);
    
    /**
     * JDBC execute timeout value, unit:second.
     */
    private int queryTimeout = 3;
    
    private static final int TRANSACTION_QUERY_TIMEOUT = 5;
    
    private static final int DB_MASTER_SELECT_THRESHOLD = 1;
    
    private static final String DB_LOAD_ERROR_MSG = "[db-load-error]load jdbc.properties error";
    
    private List<HikariDataSource> dataSourceList = new ArrayList<>();
    
    private JdbcTemplate jt;
    
    private DataSourceTransactionManager tm;
    
    private TransactionTemplate tjt;
    
    private JdbcTemplate testMasterJt;
    
    private JdbcTemplate testMasterWritableJt;
    
    private volatile List<JdbcTemplate> testJtList;
    
    private volatile List<Boolean> isHealthList;
    
    private volatile int masterIndex;
    
    private String dataSourceType = "";
    
    private final String defaultDataSourceType = "";
    
    @Override
    public void init() {
        queryTimeout = ConvertUtils.toInt(System.getProperty("QUERYTIMEOUT"), 3);
        jt = new JdbcTemplate();
        // Set the maximum number of records to prevent memory expansion
        jt.setMaxRows(50000);
        jt.setQueryTimeout(queryTimeout);
        
        testMasterJt = new JdbcTemplate();
        testMasterJt.setQueryTimeout(queryTimeout);
        
        testMasterWritableJt = new JdbcTemplate();
        // Prevent the login interface from being too long because the main library is not available
        testMasterWritableJt.setQueryTimeout(1);
        
        //  Database health check
        
        testJtList = new ArrayList<>();
        isHealthList = new ArrayList<>();
        
        tm = new DataSourceTransactionManager();
        tjt = new TransactionTemplate(tm);
        
        // Transaction timeout needs to be distinguished from ordinary operations.
        tjt.setTimeout(TRANSACTION_QUERY_TIMEOUT);
        
        dataSourceType = DatasourcePlatformUtil.getDatasourcePlatform(defaultDataSourceType);
        
        if (DatasourceConfiguration.isUseExternalDb()) {
            try {
                reload();
            } catch (IOException e) {
                LOGGER.error("[ExternalDataSourceService] datasource reload error", e);
                throw new RuntimeException(DB_LOAD_ERROR_MSG, e);
            }
            
            if (this.dataSourceList.size() > DB_MASTER_SELECT_THRESHOLD) {
                PersistenceExecutor.scheduleTask(new SelectMasterTask(), 10, 10, TimeUnit.SECONDS);
            }
            PersistenceExecutor.scheduleTask(new CheckDbHealthTask(), 10, 10, TimeUnit.SECONDS);
        }
    }
    
    @Override
    public synchronized void reload() throws IOException {
        try {
            final List<JdbcTemplate> testJtListNew = new ArrayList<JdbcTemplate>();
            final List<Boolean> isHealthListNew = new ArrayList<Boolean>();
            
            List<HikariDataSource> dataSourceListNew = new ExternalDataSourceProperties()
                    .build(EnvUtil.getEnvironment(), (dataSource) -> {
                        //check datasource connection
                        ConnectionCheckUtil.checkDataSourceConnection(dataSource);
                        
                        JdbcTemplate jdbcTemplate = new JdbcTemplate();
                        jdbcTemplate.setQueryTimeout(queryTimeout);
                        jdbcTemplate.setDataSource(dataSource);
                        testJtListNew.add(jdbcTemplate);
                        isHealthListNew.add(Boolean.TRUE);
                    });
            
            final List<HikariDataSource> dataSourceListOld = dataSourceList;
            final List<JdbcTemplate> testJtListOld = testJtList;
            dataSourceList = dataSourceListNew;
            testJtList = testJtListNew;
            isHealthList = isHealthListNew;
            new SelectMasterTask().run();
            new CheckDbHealthTask().run();
            
            //close old datasource.
            if (dataSourceListOld != null && !dataSourceListOld.isEmpty()) {
                for (HikariDataSource dataSource : dataSourceListOld) {
                    dataSource.close();
                }
            }
            if (testJtListOld != null && !testJtListOld.isEmpty()) {
                for (JdbcTemplate oldJdbc : testJtListOld) {
                    oldJdbc.setDataSource(null);
                }
            }
        } catch (RuntimeException e) {
            LOGGER.error(DB_LOAD_ERROR_MSG, e);
            throw new IOException(e);
        }
    }
    
    @Override
    public boolean checkMasterWritable() {
        
        testMasterWritableJt.setDataSource(jt.getDataSource());
        // Prevent the login interface from being too long because the main library is not available
        testMasterWritableJt.setQueryTimeout(1);
        String sql = " SELECT @@read_only ";
        
        try {
            Integer result = testMasterWritableJt.queryForObject(sql, Integer.class);
            if (result == null) {
                return false;
            } else {
                return result == 0;
            }
        } catch (CannotGetJdbcConnectionException e) {
            LOGGER.error("[db-error] " + e, e);
            return false;
        }
        
    }
    
    @Override
    public JdbcTemplate getJdbcTemplate() {
        return this.jt;
    }
    
    @Override
    public TransactionTemplate getTransactionTemplate() {
        return this.tjt;
    }
    
    @Override
    public String getCurrentDbUrl() {
        DataSource ds = this.jt.getDataSource();
        if (ds == null) {
            return StringUtils.EMPTY;
        }
        HikariDataSource bds = (HikariDataSource) ds;
        return bds.getJdbcUrl();
    }
    
    @Override
    public String getHealth() {
        for (int i = 0; i < isHealthList.size(); i++) {
            if (!isHealthList.get(i)) {
                if (i == masterIndex) {
                    // The master is unhealthy.
                    return "DOWN:" + InternetAddressUtil.getIpFromString(dataSourceList.get(i).getJdbcUrl());
                } else {
                    // The slave  is unhealthy.
                    return "WARN:" + InternetAddressUtil.getIpFromString(dataSourceList.get(i).getJdbcUrl());
                }
            }
        }
        
        return "UP";
    }
    
    @Override
    public String getDataSourceType() {
        return dataSourceType;
    }
    
    class SelectMasterTask implements Runnable {
        
        @Override
        public void run() {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("check master db.");
            }
            boolean isFound = false;
            
            int index = -1;
            for (HikariDataSource ds : dataSourceList) {
                index++;
                testMasterJt.setDataSource(ds);
                testMasterJt.setQueryTimeout(queryTimeout);
                try {
                    testMasterJt.update("DELETE FROM config_info WHERE data_id='com.alibaba.nacos.testMasterDB'");
                    if (jt.getDataSource() != ds) {
                        LOGGER.warn("[master-db] {}", ds.getJdbcUrl());
                    }
                    jt.setDataSource(ds);
                    tm.setDataSource(ds);
                    isFound = true;
                    masterIndex = index;
                    break;
                } catch (DataAccessException e) { // read only
                    LOGGER.warn("[master-db] master db access error", e);
                }
            }
            
            if (!isFound) {
                LOGGER.error("[master-db] master db not found.");
                DatasourceMetrics.getDbException().increment();
            }
        }
    }
    
    class CheckDbHealthTask implements Runnable {
        
        @Override
        public void run() {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("check db health.");
            }
            String sql = "SELECT * FROM config_info_gray WHERE id = 1";
            
            for (int i = 0; i < testJtList.size(); i++) {
                JdbcTemplate jdbcTemplate = testJtList.get(i);
                try {
                    try {
                        jdbcTemplate.queryForMap(sql);
                    } catch (EmptyResultDataAccessException e) {
                        // do nothing.
                    }
                    isHealthList.set(i, Boolean.TRUE);
                } catch (DataAccessException e) {
                    if (i == masterIndex) {
                        LOGGER.error("[db-error] master db {} down.",
                                InternetAddressUtil.getIpFromString(dataSourceList.get(i).getJdbcUrl()));
                    } else {
                        LOGGER.error("[db-error] slave db {} down.",
                                InternetAddressUtil.getIpFromString(dataSourceList.get(i).getJdbcUrl()));
                    }
                    isHealthList.set(i, Boolean.FALSE);
                    
                    DatasourceMetrics.getDbException().increment();
                }
            }
        }
    }
}
