package com.bsmlab.dfx.agent.config;

import com.bsmlab.dfx.agent.DfxAgentApplication;
import com.bsmlab.dfx.agent.config.datasource.DynamicDataSourceService;
import com.bsmlab.dfx.agent.config.datasource.DynamicRoutingDataSource;
import com.bsmlab.dfx.agent.config.datasource.RefreshableSqlSessionFactoryBean;
import io.micrometer.common.util.StringUtils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.annotation.MapperScans;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;

@Slf4j
@Getter
@Configuration
@RequiredArgsConstructor
@MapperScans({
        @MapperScan("com.bsmlab.dfx.agent")
})
public class DfxAgentConfiguration {
    // gradle bootRun 실행 설정
    //   bootRun  D:\projects\bsm-lab\dfx\dfxagent\src\docs\settings-examples\dfxagent.json"
    // command line java 실행 설정
    //   java -jar dfxagent.jar --setting.file=D:\projects\bsm-lab\dfx\dfxagent\src\docs\settings-examples\dfxagent.json

    //example: setting.file=D:\projects\bsm-lab\dfx\dfxagent\src\docs\settings-examples\dfxagent.json
    @Value("${setting.file}")
    private String settingFile;
    private String pid;

    private Map<String, SqlSessionFactory> temporarySqlSessionFactoryMap = new HashMap<>();
    Map<String, DataSourceTransactionManager> temporaryTransactionManagerMap = new HashMap<>();

    // agent 설정 관리자. 대부분의 기능에 필요함
    @Bean(name = "agentConfigReader")
    public AgentConfigReader agentConfigReader(ConfigurableApplicationContext configurableApplicationContext) throws URISyntaxException, IOException { // 실행확인됨
        if(StringUtils.isBlank(this.settingFile)) {
            log.error("cannot found a setting file. {}", this.settingFile);
            log.error("exit application");
            System.exit(0);
        }
        AgentConfigReader agentConfigReader = new AgentConfigReader(configurableApplicationContext);
        agentConfigReader.loadConfigFile(this.settingFile);
        // Process ID를 찾아 app_home/proc/pid 파일에 기록한다. 이후 shutdown.sh에서 해당 pid를 사용하여 kill -9 pid 를 실행한다.
        this.pid = String.valueOf(ProcessHandle.current().pid());
        URL location = DfxAgentApplication.class.getProtectionDomain().getCodeSource().getLocation();
        URI uri = location.toURI();
        File file;
        if ("file".equalsIgnoreCase(uri.getScheme())) {
            // 파일 시스템 상의 경로
            file = new File(uri);
        }
        else if ("jar".equalsIgnoreCase(uri.getScheme()) || location.toString().startsWith("jar:")) {
            // JAR 내부 경로일 경우
            String path = location.toString();
            if (path.startsWith("jar:")) {
                path = path.substring(4, path.indexOf("!") - 1);
                path = path.substring(path.indexOf(":") + 1, path.lastIndexOf("/"));
            }
            file = new File(path);
        }
        else {
            // 파일 시스템 상의 경로
            file = new File(uri);
        }
        String procDirectoryPath = file.getParent() + File.separator + "proc";
        File procFile = new File(procDirectoryPath);
        if(!procFile.exists()) {
            procFile.mkdirs();
        }
        Path pidFilePath = Path.of(procDirectoryPath + File.separator, "dfxagent.pid");
        Files.writeString(pidFilePath, pid, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
        return agentConfigReader;
    }

    // 다중 데이터 소스 생성
    @Bean(name = "dynamicRoutingDataSource")
    public DynamicRoutingDataSource dynamicRoutingDataSource(AgentConfigReader agentConfigReader) throws IOException { // 실행확인됨
        DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
        Map<String, SqlSessionFactory> sqlSessionFactoryMap = new HashMap<>();
        Map<String, DataSourceTransactionManager> transactionManagerMap = new HashMap<>();

        List<String> xmlPathList = agentConfigReader.getAgentConfigDto().getSqlMapperLocations();
        List<Resource> resourceList = new ArrayList<>();
        PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
        for(String pathString : xmlPathList) {
            Resource[] resourceArray = pathMatchingResourcePatternResolver.getResources("file:" + pathString);
            resourceList.addAll(Arrays.asList(resourceArray));
        }

        List<AgentConfigDto.DataSourceConfig> dataSourceConfigList = agentConfigReader.getAgentConfigDto().getDataSourceConfig();
        for(AgentConfigDto.DataSourceConfig dataSourceConfig : dataSourceConfigList) {
            try {
                BasicDataSource dataSource = DynamicDataSourceService.createBasicDataSource(dataSourceConfig);
                SqlSessionFactoryBean sqlSessionFactoryBean = new RefreshableSqlSessionFactoryBean();//new SqlSessionFactoryBean();
                org.apache.ibatis.session.Configuration mybatisConfiguration = new org.apache.ibatis.session.Configuration();
                mybatisConfiguration.setJdbcTypeForNull(JdbcType.NULL);
                sqlSessionFactoryBean.setConfiguration(mybatisConfiguration);
                sqlSessionFactoryBean.setFailFast(true);
                sqlSessionFactoryBean.setDataSource(dataSource);
                sqlSessionFactoryBean.setMapperLocations(resourceList.toArray(new Resource[0]));
                sqlSessionFactoryBean.afterPropertiesSet();
                sqlSessionFactoryMap.put(dataSourceConfig.getDataSourceId(), sqlSessionFactoryBean.getObject());
                DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
                transactionManagerMap.put(dataSourceConfig.getDataSourceId(), transactionManager);
                dynamicRoutingDataSource.addDataSource(dataSourceConfig.getDataSourceId(), dataSource);
            } catch (Exception e) {
                log.error("DynamicRoutingDataSource 생성 중 오류: {}", e.getMessage(), e);
                throw new RuntimeException(e);
            }
        }
        this.temporarySqlSessionFactoryMap = sqlSessionFactoryMap;
        this.temporaryTransactionManagerMap = transactionManagerMap;
        return dynamicRoutingDataSource;
    }

    // 다중 데이터 소스와 그에 해당하는 sqlSession, transactionManager 설정
    @Bean(name = "dynamicDataSourceService")
    public DynamicDataSourceService dynamicDataSourceService() { // 실행확인됨
        DynamicDataSourceService dynamicDataSourceService = new DynamicDataSourceService();
        dynamicDataSourceService.setSqlSessionFactoryMap(this.temporarySqlSessionFactoryMap);
        dynamicDataSourceService.setTransactionManagerMap(this.temporaryTransactionManagerMap);
        return dynamicDataSourceService;
    }

    // (수신 처리) 메시지 수신 - 저장 후 수신 메시지 처리 쓰레드 설정
    @Bean(name = "dropBoxProcessorThreadPoolTaskScheduler")
    public ThreadPoolTaskScheduler dropBoxProcessorThreadPoolTaskScheduler(AgentConfigReader agentConfigReader) {
        ThreadPoolTaskScheduler dropBoxProcessorThreadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        dropBoxProcessorThreadPoolTaskScheduler.setPoolSize(agentConfigReader.getAgentConfigDto().getDropBoxConfig().getThreadPoolSize());
        dropBoxProcessorThreadPoolTaskScheduler.setThreadNamePrefix("dropBoxProcessor-");
        dropBoxProcessorThreadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true); // 종료 시 대기 여부
        dropBoxProcessorThreadPoolTaskScheduler.initialize();
        return dropBoxProcessorThreadPoolTaskScheduler;
    }

    // (송신 처리) 메시지 송신 쓰레드 설정
    @Bean(name = "scheduledPostmanThreadPoolTaskScheduler")
    public ThreadPoolTaskScheduler scheduledPostmanThreadPoolTaskScheduler(AgentConfigReader agentConfigReader) { // 실행확인됨
        ThreadPoolTaskScheduler scheduledPostmanThreadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        scheduledPostmanThreadPoolTaskScheduler.setPoolSize(agentConfigReader.getAgentConfigDto().getPostmanConfig().getThreadPoolSize());
        scheduledPostmanThreadPoolTaskScheduler.setThreadNamePrefix("postman-scheduler-");
        scheduledPostmanThreadPoolTaskScheduler.initialize();
        return scheduledPostmanThreadPoolTaskScheduler;
    }

    // 연결된 agent 상태 확인 쓰레드 설정
    @Bean(name = "statusCheckerThreadPoolTaskScheduler")
    public ThreadPoolTaskScheduler statusCheckerThreadPoolTaskScheduler() {
        ThreadPoolTaskScheduler statusCheckerThreadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        statusCheckerThreadPoolTaskScheduler.setPoolSize(1);
        statusCheckerThreadPoolTaskScheduler.setThreadNamePrefix("status-checker-");
        statusCheckerThreadPoolTaskScheduler.initialize();
        return statusCheckerThreadPoolTaskScheduler;
    }

    // 파일 정리 쓰레드 설정
    @Bean(name = "fileCleanerThreadPoolTaskScheduler")
    public ThreadPoolTaskScheduler fileCleanerThreadPoolTaskScheduler() {
        ThreadPoolTaskScheduler fileCleanerThreadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        fileCleanerThreadPoolTaskScheduler.setPoolSize(1);
        fileCleanerThreadPoolTaskScheduler.setThreadNamePrefix("file-cleaner-");
        fileCleanerThreadPoolTaskScheduler.initialize();
        return fileCleanerThreadPoolTaskScheduler;
    }
}
