package ru.nbch.indicsearchservice.util;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.sql.DataSource;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;
import ru.nbch.indicsearchservice.config.settings.ApplicationSettings;
import ru.nbch.indicsearchservice.config.settings.DataSourceSettings;
import ru.nbch.indicsearchservice.event.DataSourceChangedEvent;
import ru.nbch.indicsearchservice.repository.MyBatisRepository;
import ru.nbch.indicsearchservice.repository.indic.IndicMapper;
import ru.nbch.indicsearchservice.service.SettingsService;
import ru.nbch.indicsearchservice.util.typehandler.NullableLocalDateTimeTypeHandler;
import ru.nbch.indicsearchservice.util.typehandler.NullableLocalDateTypeHandler;

@Component
public class DataSourceUpdater {
    private static final Logger log = LoggerFactory.getLogger(DataSourceUpdater.class);

    private final ApplicationSettings settings;
    private final Map<DataSourceType, DataSourceData> dataSourceMap = new ConcurrentHashMap<>();

    public DataSourceUpdater(ApplicationSettings settings, SettingsService settingsService) {
        this.settings = settings;

        buildSettings(DataSourceType.INDIC, settingsService, settings::setIndicDataSource);

        dataSourceMap.put(
            DataSourceType.INDIC,
            new DataSourceData(new AtomicReference<>(null), new AtomicReference<>(null), new AtomicReference<>(null))
        );
    }

    @EventListener
    public void onDatabaseChanged(DataSourceChangedEvent event) {
        log.info("Connection to datasource {} invalidated", event.dataSourceType());

        if (!dataSourceMap.containsKey(event.dataSourceType())) {
            log.error("Unknown datasource type {}", event.dataSourceType());
            return;
        }

        DataSource oldDataSource = dataSourceMap.get(event.dataSourceType()).dataSourceRef().getAndSet(null);
        if (oldDataSource instanceof HikariDataSource ds) {
            ds.close();
        }

        dataSourceMap.get(event.dataSourceType()).sqlSessionFactoryRef().set(null);
        dataSourceMap.get(event.dataSourceType()).mapperRef().set(null);
    }

    public DataSource getDataSource(DataSourceType dataSourceType) {
        if (!dataSourceMap.containsKey(dataSourceType)) {
            log.error("Unknown datasource type {}", dataSourceType);
            return null;
        }

        return getComponent(dataSourceMap.get(dataSourceType).dataSourceRef(), () -> buildDataSource(dataSourceType), "DataSource");
    }

    public SqlSessionFactory getSqlSessionFactory(DataSourceType dataSourceType) {
        if (!dataSourceMap.containsKey(dataSourceType)) {
            log.error("Unknown datasource type {}", dataSourceType);
            return null;
        }

        return getComponent(dataSourceMap.get(dataSourceType).sqlSessionFactoryRef(), () -> buildSqlSessionFactory(dataSourceType), "SqlSessionFactory");
    }

    public MyBatisRepository getMapper(DataSourceType dataSourceType) {
        if (!dataSourceMap.containsKey(dataSourceType)) {
            log.error("Unknown datasource type {}", dataSourceType);
            return null;
        }

        return getComponent(dataSourceMap.get(dataSourceType).mapperRef(), () -> buildMapper(dataSourceType), "Mapper");
    }

    private <T> T getComponent(AtomicReference<T> ref, Supplier<T> builder, String componentName) {
        T component = ref.get();
        if (component == null) {
            synchronized (ref) {
                component = ref.get();
                if (component == null) {
                    try {
                        component = builder.get();
                        ref.set(component);
                        log.info("{} created successfully", componentName);
                    } catch (Exception e) {
                        log.error("Failed to build {}", componentName, e);
                    }
                }
            }
        }

        return component;
    }

    private DataSource buildDataSource(DataSourceType dataSourceType) {
        DataSourceSettings dataSourceSettings = switch (dataSourceType) {
            case INDIC -> settings.getIndicDataSource();
        };

        if (dataSourceSettings == null) {
            log.error("{} configuration is null", dataSourceType);
            return null;
        }

        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName(dataSourceSettings.getDriver());
        hikariConfig.setJdbcUrl(dataSourceSettings.getUrl());
        hikariConfig.setUsername(dataSourceSettings.getUsername());
        hikariConfig.setPassword(dataSourceSettings.getPassword());

        hikariConfig.setConnectionTimeout(dataSourceSettings.getConnectionTimeout());
        hikariConfig.setValidationTimeout(dataSourceSettings.getValidationTimeout());
        hikariConfig.setIdleTimeout(dataSourceSettings.getIdleTimeout());
        hikariConfig.setMaximumPoolSize(dataSourceSettings.getMaxPoolSize());
        hikariConfig.setMaxLifetime(dataSourceSettings.getMaxLifetime());
        hikariConfig.setKeepaliveTime(dataSourceSettings.getKeepAliveTime());

        hikariConfig.setPoolName(dataSourceType + "-pool");

        if (Objects.equals(dataSourceSettings.getDriver(), "net.sourceforge.jtds.jdbc.Driver")) {
            hikariConfig.setConnectionTestQuery("select 1");
        }

        return new HikariDataSource(hikariConfig);
    }

    private SqlSessionFactory buildSqlSessionFactory(DataSourceType dataSourceType) {
        DataSource dataSource = getDataSource(dataSourceType);
        try {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSource);

            Configuration configuration = new Configuration();
            configuration.setMapUnderscoreToCamelCase(true);
            configuration.setCallSettersOnNulls(true);
            configuration.getTypeHandlerRegistry()
                .register(LocalDate.class, JdbcType.DATE, new NullableLocalDateTypeHandler());
            configuration.getTypeHandlerRegistry()
                .register(LocalDateTime.class, JdbcType.TIMESTAMP, new NullableLocalDateTimeTypeHandler());

            sqlSessionFactoryBean.setConfiguration(configuration);

            if (dataSourceType == DataSourceType.INDIC) {
                sqlSessionFactoryBean.setMapperLocations(
                    new PathMatchingResourcePatternResolver()
                        .getResources("classpath:mappers/indic/*.xml")
                );
            } else {
                Class<?> mapperClass = getMapperClass(dataSourceType);
                configuration.addMapper(mapperClass);
            }

            SqlSessionFactory sessionFactory = sqlSessionFactoryBean.getObject();
            if (sessionFactory == null) {
                throw new IllegalStateException("Failed to create SqlSessionFactory");
            }

            return sessionFactory;
        } catch (Exception e) {
            log.error("Failed to build SqlSessionFactory", e);
            throw new RuntimeException("Failed to build SqlSessionFactory", e);
        }
    }

    private <T extends MyBatisRepository> T buildMapper(DataSourceType dataSourceType) {
        Class<?> mapperClass = getMapperClass(dataSourceType);
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(dataSourceType);
        SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
        return (T) sqlSessionTemplate.getMapper(mapperClass);
    }

    private Class<?> getMapperClass(DataSourceType dataSourceType) {
        return switch (dataSourceType) {
            case INDIC -> IndicMapper.class;
        };
    }

    private void buildSettings(DataSourceType dataSourceType, SettingsService settingsService, Consumer<DataSourceSettings> settingsConsumer) {
        String key = switch (dataSourceType) {
            case INDIC -> "indic";
        };

        DataSourceSettings dataSourceSettings = new DataSourceSettings();
        dataSourceSettings.setDriver(settingsService.getSettingsValueByName("datasource." + key + ".driver"));
        dataSourceSettings.setUrl(settingsService.getSettingsValueByName("datasource." + key + ".url"));
        dataSourceSettings.setUsername(settingsService.getSettingsValueByName("datasource." + key + ".username"));
        dataSourceSettings.setPassword(settingsService.getSettingsValueByName("datasource." + key + ".password"));

        dataSourceSettings.setConnectionTimeout(Long.valueOf(settingsService.getOptionalSettingsValueByName("datasource." + key + ".connection-timeout").orElse("30000")));
        dataSourceSettings.setValidationTimeout(Long.valueOf(settingsService.getOptionalSettingsValueByName("datasource." + key + ".validation-timeout").orElse("5000")));
        dataSourceSettings.setIdleTimeout(Long.valueOf(settingsService.getOptionalSettingsValueByName("datasource." + key + ".idle-timeout").orElse("600000")));
        dataSourceSettings.setMaxPoolSize(Integer.valueOf(settingsService.getOptionalSettingsValueByName("datasource." + key + ".max-pool-size").orElse("100")));
        dataSourceSettings.setMaxLifetime(Long.valueOf(settingsService.getOptionalSettingsValueByName("datasource." + key + ".max-lifetime").orElse("1800000")));
        dataSourceSettings.setKeepAliveTime(Long.valueOf(settingsService.getOptionalSettingsValueByName("datasource." + key + ".keep-alive-time").orElse("120000")));

        settingsConsumer.accept(dataSourceSettings);
    }

    private record DataSourceData(AtomicReference<DataSource> dataSourceRef,
                                  AtomicReference<SqlSessionFactory> sqlSessionFactoryRef,
                                  AtomicReference<MyBatisRepository> mapperRef) {
    }

    public enum DataSourceType {
        INDIC
    }
}
