package com.bsmlab.dfx.agent;

import com.bsmlab.dfx.agent.config.AgentConfigDto;
import com.bsmlab.dfx.agent.config.AgentConfigReader;
import com.bsmlab.dfx.agent.config.datasource.DynamicRoutingDataSource;
import com.bsmlab.dfx.agent.listener.dto.AckDto;
import com.bsmlab.dfx.agent.listener.dto.CommandDto;
import com.bsmlab.dfx.agent.listener.dto.ReceiveMessageDto;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

import javax.sql.DataSource;
import java.io.*;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

@Slf4j
@RequiredArgsConstructor
@Component
public class DfxAgentRunner implements ApplicationRunner {
    private final AgentConfigReader agentConfigReader;
    private final DynamicRoutingDataSource dynamicRoutingDataSource;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // command line 실행 중 --check-config 가 있는 경우 settings.json 을 로드하여 DB 연결, knownAgent 연결 테스트를 진행한다.
        // java -jar $AGENT_HOME/lib/dfxagent.jar --setting.check --setting.file=$AGENT_HOME/conf/settings.json &
        if(args.containsOption("setting.check")) {
            try {
                this.checkConfig(args);
            }
            catch(Exception e) {
                System.err.println(e.getLocalizedMessage());
                System.exit(2);
            }
            System.exit(0);
        }
        else if(args.containsOption("parse.message.file")) {
            try {
                this.parseMessageFile(args);
            }
            catch(Exception e) {
                System.err.println(e.getLocalizedMessage());
                System.exit(2);
            }
            System.exit(0);
        }
    }

    private void print(String text) {
        System.out.println(text);
    }

    private void parseMessageFile(ApplicationArguments args) throws IOException {
        OutputStream outputStream = null;
        try {
            this.print("-----------------------");
            this.print("변환을 시작합니다.");
            File sourceFile = new File(args.getOptionValues("parse.message.file").get(0));
            this.print("메시지 파일: " + sourceFile.getAbsoluteFile());
            if(args.getOptionValues("output") != null && !args.getOptionValues("output").isEmpty()) {
                File outputFile = new File(args.getOptionValues("output").get(0));
                this.print("결과저장: " + outputFile.getAbsoluteFile());
                outputStream = new FileOutputStream(outputFile);
            }
            else {
                this.print("결과저장: 화면출력");
                outputStream = System.out;
            }
            this.print("-----------------------");
            if(!sourceFile.exists()) {
                new FileNotFoundException("cannot find: " + sourceFile.getAbsolutePath());
            }
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(sourceFile));
            ReceiveMessageDto receiveMessageDto = (ReceiveMessageDto) objectInputStream.readObject();
            ObjectMapper objectMapper = new ObjectMapper();
            String parsedString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(receiveMessageDto);
            outputStream.write(parsedString.getBytes());
            if(outputStream == System.out) {
                outputStream.flush();
            }
            else {
                outputStream.close();
            }
        }
        catch(FileNotFoundException e) {
            this.print("오류가 포함되어 있습니다.");
            this.print(e.getLocalizedMessage());
            System.exit(2);
        }
        catch (ClassNotFoundException e) {
            this.print("오류가 포함되어 있습니다.");
            this.print(e.getLocalizedMessage());
            System.exit(2);
        }
        catch (JsonProcessingException e) {
            this.print("오류가 포함되어 있습니다.");
            this.print(e.getLocalizedMessage());
            System.exit(2);
        }
        catch (IOException e) {
            this.print("오류가 포함되어 있습니다.");
            this.print(e.getLocalizedMessage());
            System.exit(2);
        }
        this.print("-----------------------");
        this.print("변환을 종료합니다.");
        this.print("-----------------------");
        System.exit(0);
    }

    private void checkConfig(ApplicationArguments args) {
        this.print("-----------------------");
        this.print("DfxAgent 설정 및 환경 점검");
        this.print("-----------------------");
        this.print("    (설정 파일: " + args.getOptionValues("setting.file") + ")");
        // settings.json 점검 로직
        boolean isValid = true;

        this.print("");
        this.print("데이터베이스 설정 점검");
        Map<Object, Object> dataSourceMap = dynamicRoutingDataSource.getDataSourceMap();
        if(dataSourceMap != null && !dataSourceMap.isEmpty()) {
            for(Object key : dataSourceMap.keySet()) {
                DataSource dataSource = (DataSource) dataSourceMap.get(key);
                AgentConfigDto.DataSourceConfig dataSourceConfig = null;
                List<AgentConfigDto.DataSourceConfig> dataSourceConfigList = agentConfigReader.getAgentConfigDto().getDataSourceConfig();
                for(AgentConfigDto.DataSourceConfig thisDataSourceConfig : dataSourceConfigList) {
                    if(thisDataSourceConfig.getDataSourceId().equals(key)) {
                        dataSourceConfig = thisDataSourceConfig;
                        break;
                    }
                }
                this.print("DB 점검 [" + dataSourceConfig.toString() + "]");
                try(Connection connection = dataSource.getConnection()) {
                    connection.isValid(10);
                    this.print("DB 연결에 성공하였습니다.");
                }
                catch(SQLException e) {
                    this.print("DB 연결에 실패하였습니다. (" + e.getLocalizedMessage() + ")");
                    isValid = false;
                }
                catch(Exception e) {
                    this.print("DB 점검에 실패하였습니다. (" + e.getLocalizedMessage() + ")");
                    isValid = false;
                }
            }
        }
        else {
            this.print("데이터베이스 설정은 포함되어 있지 않습니다.");
            isValid = false;
        }

        this.print("");
        this.print("상대 agent 설정 점검");
        List<AgentConfigDto.KnownAgent> knownAgentList = agentConfigReader.getAgentConfigDto().getKnownAgentList();
        if(knownAgentList != null && !knownAgentList.isEmpty()) {
            Map<String, AgentConfigDto.KnownAgent> knownAgentMap = new HashMap<>();
            for(AgentConfigDto.KnownAgent knownAgent : knownAgentList) {
                knownAgentMap.put(knownAgent.getHostId(), knownAgent);
                try {
                    String messageUuid = UUID.randomUUID().toString();
                    CommandDto commandDto = CommandDto.builder().commandType(CommandDto.CommandType.ALIVE).messageUuid(messageUuid).build();
                    HttpHeaders httpHeaders = new HttpHeaders();
                    httpHeaders.setContentType(MediaType.APPLICATION_JSON);
                    httpHeaders.set("User-Agent", agentConfigReader.getApplicationName() + ", version: " + agentConfigReader.getApplicationVersion() + "(" + agentConfigReader.getApplicationCommitId() + ")"
                            + ", host ID: " + agentConfigReader.getAgentConfigDto().getMyHostId()
                            + ", action: agent-check(ALIVE: command line)"
                    );
                    HttpEntity<CommandDto> bodyEntity = new HttpEntity<>(commandDto, httpHeaders);
                    RestTemplate restTemplate = new RestTemplate();
                    String url = "http://" + knownAgent.getHostName() + ":" + knownAgent.getListenPort() + "/command";
                    this.print("상대 agent " + knownAgent.getHostId() + "(" + knownAgent.getHostName() + ":" + knownAgent.getListenPort() + ") 연결 점검");
                    String response = restTemplate.postForObject(url, bodyEntity, String.class);
                    ObjectMapper objectMapper = new ObjectMapper();
                    AckDto ackDto = null;
                    ackDto = objectMapper.readValue(response, new TypeReference<AckDto>() {});
                    if(AckDto.ResultType.PROCESS_SUCCESS == ackDto.getResult()) {
                        this.print("상대 agent " + knownAgent.getHostId() + "(" + knownAgent.getHostName() + "가 정상 동작합니다.");
                        commandDto.setCommandType(CommandDto.CommandType.INFORMATION);
                        bodyEntity = new HttpEntity<>(commandDto, httpHeaders);
                        this.print("상대 agent " + knownAgent.getHostId() + "(" + knownAgent.getHostName() + ":" + knownAgent.getListenPort() + ") DropBox 점검");
                        response = restTemplate.postForObject(url, bodyEntity, String.class);
                        ackDto = objectMapper.readValue(response, new TypeReference<AckDto>() {});
                        AgentConfigDto knownAgentConfigDto = objectMapper.readValue(ackDto.getResultText(), new TypeReference<AgentConfigDto>() {});
                        List<AgentConfigDto.DropBox> knownAgentDropBoxList = knownAgentConfigDto.getDropBoxConfig().getDropBoxList();
                        if(knownAgent.getDropBoxIdList() != null) {
                            if(knownAgentDropBoxList != null) {
                                for(String dropBoxId : knownAgent.getDropBoxIdList()) {
                                    boolean isHasDropBoxId = false;
                                    for(AgentConfigDto.DropBox knwonAgentDropBox : knownAgentDropBoxList) {
                                        if(dropBoxId.equals(knwonAgentDropBox.getDropBoxId())) {
                                            isHasDropBoxId = true;
                                        }
                                    }
                                    if(!isHasDropBoxId) {
                                        this.print("DropBoxId " + dropBoxId + "에 해당하는 상대 agent " + knownAgent.getHostId() + "(" + knownAgent.getHostName() + ")의 DropBoxId가 존재하지 않습니다.");
                                        isValid = false;
                                    }
                                }
                            }
                            else {
                                this.print("상대 agent " + knownAgent.getHostId() + "(" + knownAgent.getHostName() + ")의 DropBox 설정이 존재하지 않습니다.");
                                isValid = false;
                            }
                        }
                    }
                    else {
                        this.print("상대 agent " + knownAgent.getHostId() + "(" + knownAgent.getHostName() + ")가 응답하였으나 정상 동작하지 않습니다.");
                        isValid = false;
                    }
                }
                catch (JsonProcessingException e) {
                    this.print("상대 agent " + knownAgent.getHostId() + "(" + knownAgent.getHostName() + ")가 응답메시지가 정상적이지 않습니다. (" + e.getLocalizedMessage() + ")");
                    isValid = false;
                }
                catch (ResourceAccessException e) {
                    this.print("상대 agent " + knownAgent.getHostId() + "(" + knownAgent.getHostName() + ")가 응답하지 않습니다. (" + e.getLocalizedMessage() + ")");
                    isValid = false;
                }
                catch (Exception e) {
                    this.print("상대 agent " + knownAgent.getHostId() + "(" + knownAgent.getHostName() + ") 상태를 알 수 없습니다. (" + e.getLocalizedMessage() + ")");
                    isValid = false;
                }
            }

            this.print("");
            this.print("Postman 설정 점검");
            if(agentConfigReader.getAgentConfigDto().getPostmanConfig() != null) {
                this.print("Postman thread pool size: " + agentConfigReader.getAgentConfigDto().getPostmanConfig().getThreadPoolSize());
                List<AgentConfigDto.Postman> postManList = agentConfigReader.getAgentConfigDto().getPostmanConfig().getPostmanList();
                if(postManList != null) {
                    for(AgentConfigDto.Postman postman : postManList) {
                        AgentConfigDto.KnownAgent knownAgent = knownAgentMap.get(postman.getRecipientHostId());
                        if(knownAgent != null) {
                            boolean isHasDropBoxId = false;
                            for(String knownAgentDropBoxId : knownAgent.getDropBoxIdList()) {
                                if(postman.getRecipientDropBoxId().equals(knownAgentDropBoxId)) {
                                    isHasDropBoxId = true;
                                }
                            }
                            if(!isHasDropBoxId) {
                                this.print("postman " + postman.getPostmanId() + "에 대한 recipientDropBoxId(" + postman.getRecipientDropBoxId() + ")가 knownAgent(" + knownAgent.getHostId() + ") dropBoxIdList 목록에 존재하지 않습니다.");
                                isValid = false;
                            }
                        }
                        else {
                            this.print("postman " + postman.getPostmanId() + "에 대한 recipientHostId(" + postman.getRecipientHostId() + ")가 knownAgent 목록에 존재하지 않습니다.");
                            isValid = false;
                        }
                    }
                }
                else {
                    this.print("Postman 목록 없음");
                }
            }
            else {
                this.print("Postman 설정 없음");
                isValid = false;
            }
        }
        else {
            this.print("상대 agent 설정은 포함되어 있지 않습니다.");
            isValid = false;
        }

        this.print("");
        this.print("--------------------------------------");
        if(isValid) {
            this.print("설정과 환경이 정상적입니다.");
            this.print("점검을 종료합니다.");
            System.exit(0);
        }
        else {
            this.print("설정 또는 환경에 오류가 포함되어 있습니다.");
            this.print("점검을 종료합니다.");
            System.exit(2);
        }
        this.print("--------------------------------------");
    }
}

