package com.bsmlab.dfx.agent.task.postman;

import com.bsmlab.dfx.agent.config.AgentConfigDto;
import com.bsmlab.dfx.agent.config.AgentConfigReader;
import com.bsmlab.dfx.agent.config.datasource.SqlExecuteService;
import com.bsmlab.dfx.agent.listener.dto.AckDto;
import com.bsmlab.dfx.agent.listener.dto.ReceiveMessageDto;
import com.bsmlab.dfx.agent.support.MessageUtils;
import com.bsmlab.dfx.agent.support.exception.DfxException;
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.apache.commons.lang3.StringUtils;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ScheduledFuture;

@Service
@Slf4j
@RequiredArgsConstructor
public class PostmanSchedulerService {
    private final ThreadPoolTaskScheduler scheduledPostmanThreadPoolTaskScheduler;
    private final AgentConfigReader agentConfigReader;
    private final SqlExecuteService sqlExecuteService;
    private Map<String, ScheduledFuture<?>> scheduledFutureMap = new HashMap<>();
    private Map<String, AgentConfigDto.Postman> postmanMap = new HashMap<>();
    private Map<String, String> sentMessageFileMap = new HashMap<>();

    // 전달받은 PostmanId의 쓰레드 실행 (run)
    private void startPostman(String postmanId) {
        AgentConfigDto.Postman postman = this.postmanMap.get(postmanId);
        this.stop(postmanId);
        String cron = postman.getAction().getCron();
        ScheduledFuture<?> scheduledFuture = scheduledPostmanThreadPoolTaskScheduler.schedule(() -> run(postman), new CronTrigger(cron));
        scheduledFutureMap.put(postmanId, scheduledFuture);
    }

    private void stop(String postmanId) {
        if(this.scheduledFutureMap.get(postmanId) != null) {
            ScheduledFuture<?> scheduledFuture = this.scheduledFutureMap.get(postmanId);
            scheduledFuture.cancel(false);
            this.scheduledFutureMap.remove(postmanId);
        }
    }

    // Postman 설정에 따라서 모든 Postman 쓰레드 활성화 (startPostman)
    public void launch() { // 실행확인됨
        log.info("{} launch", this.getClass().getName());
        List<AgentConfigDto.Postman> postmanList = agentConfigReader.getScheduledTypePostmanList();
        if(postmanList != null) {
            for(AgentConfigDto.Postman postman : postmanList) {
                this.postmanMap.put(postman.getPostmanId(), postman);
                this.startPostman(postman.getPostmanId());
            }
        }
        this.findAndAddSentMessageFile(new File(agentConfigReader.getAgentConfigDto().getDropBoxConfig().getSentMessageStorageRoot()));
    }

    // 현재 동작중인 모든 Postman 쓰레드 중지 및 postmanMap 제거
    public void stopAll() {
        for(String postmanId : postmanMap.keySet()) {
            ScheduledFuture<?> scheduledFuture = scheduledFutureMap.remove(postmanId);
            if(scheduledFuture != null) {
                scheduledFuture.cancel(true);
            }
        }
        postmanMap.clear();
    }

    private void findAndAddSentMessageFile(File parentDirectory) {
        File[] filesOnDirectory = parentDirectory.listFiles();
        if(filesOnDirectory != null) {
            for(File file : filesOnDirectory) {
                if(file.canRead() && !".".equals(file.getName()) && !"..".equals(file.getName())) {
                    if(file.isFile() && !file.getName().contains("complete")) {
                        this.sentMessageFileMap.put(file.getName(), file.getAbsolutePath());
                    }
                    else if(file.isDirectory()) {
                        findAndAddSentMessageFile(file);
                    }
                }
            }
        }
    }

    /**
     * AckDto 수신 후 postProcessingSqlId 처리. 이후 sent 메시지 파일 이름 변경 .complete
     * @param ackDto : AckDto에 포함된 messageUuid는 이전에 보낸 메시지이므로 sentMessageFileMap에 포함되어 있어야 함
     */
    @SuppressWarnings("unchecked")
    public void makeCompleteSentMessage(AckDto ackDto) {
        log.info("makeCompleteSentMessage ackDto: {}, this.sentMessageFileMap.containsKey: {}", ackDto, this.sentMessageFileMap.containsKey(ackDto.getMessageUuid()));
        if(this.sentMessageFileMap.containsKey(ackDto.getMessageUuid())) {
            File sentMessageFile = new File(this.sentMessageFileMap.get(ackDto.getMessageUuid()));
            if(sentMessageFile.exists()) {
                try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(sentMessageFile))) {
                    ReceiveMessageDto receiveMessageDto = (ReceiveMessageDto)objectInputStream.readObject();
                    ObjectMapper objectMapper = new ObjectMapper();
                    List<Map<String, Object>> dataMapList;
                    dataMapList = (List<Map<String, Object>>) objectMapper.readValue(receiveMessageDto.getData(), List.class);
                    Map<String, Object> addParamMap = new HashMap<>();
                    addParamMap.put("messageUuid", receiveMessageDto.getMessageUuid());
                    addParamMap.put("processStatus", ackDto.getResult().toString());
                    addParamMap.put("resultText", ackDto.getResultText());
                    dataMapList.forEach(map -> map.putAll(addParamMap));
                    AgentConfigDto.Postman postman = this.postmanMap.get(receiveMessageDto.getSenderPostmanId());
                    sqlExecuteService.update(postman.getMessage().getDataSourceId(), postman.getMessage().getPostProcessingSqlId(), dataMapList);
                } catch (FileNotFoundException e) {
                    log.error("cannot find a file for sent message {}", sentMessageFile.getAbsolutePath());
                    log.error("{}", e, e);
                } catch (IOException e) {
                    log.error("cannot access a file for sent message {}", sentMessageFile.getAbsolutePath());
                    log.error("{}", e, e);
                } catch (ClassNotFoundException e) {
                    log.error("cannot translate a file for sent message {}", sentMessageFile.getAbsolutePath());
                    log.error("{}", e, e);
                }
                File completeMessageFile = new File(sentMessageFile.getAbsolutePath() + ".complete");
                sentMessageFile.renameTo(completeMessageFile);
            }
            this.sentMessageFileMap.remove(ackDto.getMessageUuid());
        }
    }

    private void writeSentMessage(ReceiveMessageDto receiveMessageDto) {
        File root = new File(agentConfigReader.getAgentConfigDto().getDropBoxConfig().getSentMessageStorageRoot());
        Date today = new Date(System.currentTimeMillis());
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd/HH");
        String targetDirectoryString = root.getAbsolutePath() + "/" + dateFormat.format(today);
        File targetDirectory = new File(targetDirectoryString);
        if(!targetDirectory.exists()) {
            targetDirectory.mkdirs();
        }
        String targetFilePath = targetDirectoryString + "/" + receiveMessageDto.getMessageUuid();
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(targetFilePath)))) {
            objectOutputStream.writeObject(receiveMessageDto);
            this.sentMessageFileMap.put(receiveMessageDto.getMessageUuid(), targetFilePath);
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void run(AgentConfigDto.Postman postman) {
        AgentConfigDto.KnownAgent knownAgent = agentConfigReader.getKnownAgent(postman.getRecipientHostId());
        if("ALIVE".equals(agentConfigReader.getKnownAgentStatus(knownAgent.getHostId()))) {
            ObjectMapper objectMapper = new ObjectMapper();
            String senderHostId = agentConfigReader.getAgentConfigDto().getMyHostId();
            long senderTimestamp = System.currentTimeMillis();
            String messageUuid = UUID.randomUUID().toString();
            // DB TO DB 전송
            if(AgentConfigDto.TaskType.DB_READ_THEN_SEND == postman.getTaskType()) {
                String dataSourceId = postman.getMessage().getDataSourceId();
                String sqlId = postman.getMessage().getSqlId();
                String response = null;
                try {
                    List<Map<String, Object>> dataMapList = sqlExecuteService.select(dataSourceId, sqlId, null);
                    if(dataMapList != null && !dataMapList.isEmpty()) {
                        String dataString = objectMapper.writeValueAsString(dataMapList);
                        List<ReceiveMessageDto.RoutingHost> routingHostList = new ArrayList<>();
                        for(String routingHostId : knownAgent.getRoutingHostIdList()) {
                            ReceiveMessageDto.RoutingHost routingHost = ReceiveMessageDto.RoutingHost.builder().hostId(routingHostId).receivedTimestamp(0).build();
                            routingHostList.add(routingHost);
                        }
                        ReceiveMessageDto receiveMessageDto = ReceiveMessageDto.builder().senderHostId(senderHostId).senderTimestamp(senderTimestamp)
                                .senderPostmanId(postman.getPostmanId()).messageUuid(messageUuid).messageType(AgentConfigDto.MessageType.TRANSFER_DB_TO_DB)
                                .recipientHostId(postman.getRecipientHostId()).recipientDropBoxId(postman.getRecipientDropBoxId())
                                .routingHostList(routingHostList)
                                .data(dataString).processStatus(ReceiveMessageDto.ProcessStatus.PROCESS_SEND).build();
                        this.writeSentMessage(receiveMessageDto);
                        log.debug("postman to {} send a message UUID {} (data count: {})", receiveMessageDto.getRecipientHostId(), receiveMessageDto.getMessageUuid(), dataMapList.size());
                        AckDto ackDto = MessageUtils.send(this.agentConfigReader, postman.getPostmanId(), receiveMessageDto);
                        log.debug("postman received ack from {} ack: {}", receiveMessageDto.getRecipientHostId(), ackDto);
                        if(ackDto.getResult() == AckDto.ResultType.RECEIVE_SUCCESS) { // 전송 완료 및 Ack 수신 후 전송한 dataMapList 에 messageUuid, processStatus, resultText를 추가하여 postProcessingSqlId 실행
                            receiveMessageDto.setProcessStatus(ReceiveMessageDto.ProcessStatus.PROCESS_RECEIVED);
                            receiveMessageDto.setReceivedTimestamp(System.currentTimeMillis());
                            String postProcessingSqlId = postman.getMessage().getPostProcessingSqlId();
                            Map<String, Object> addParamMap = new HashMap<>();
                            addParamMap.put("messageUuid", receiveMessageDto.getMessageUuid());
                            addParamMap.put("processStatus", receiveMessageDto.getProcessStatus().toString());
                            addParamMap.put("resultText", ackDto.getResultText());
                            dataMapList.forEach(map -> map.putAll(addParamMap));
                            sqlExecuteService.update(dataSourceId, postProcessingSqlId, dataMapList);
                        }
                        else if(ackDto.getResult() == AckDto.ResultType.TRANSFER_SUCCESS) {
                            receiveMessageDto.setReceivedTimestamp(System.currentTimeMillis());
                            receiveMessageDto.setProcessStatus(ReceiveMessageDto.ProcessStatus.PROCESS_SEND);
                        }
                        else if(ackDto.getResult() == AckDto.ResultType.RECEIVE_FAIL || ackDto.getResult() == AckDto.ResultType.TRANSFER_FAIL) { // 전송 완료 및 Nak 수신 후 전송한 dataMapList 에 messageUuid, processStatus, resultText를 추가하여 postProcessingSqlId 실행
                            log.warn("대상 agent {}[{}]에게 전송하였으나 상대방이 수신하지 못하였습니다. 응답: {} 응답메시지: {}", knownAgent.getHostId(), knownAgent.getHostName(), ackDto.getResult(), ackDto.getResultText());
                            receiveMessageDto.setProcessStatus(ReceiveMessageDto.ProcessStatus.PROCESS_FAIL);
                            String postProcessingSqlId = postman.getMessage().getPostProcessingSqlId();
                            Map<String, Object> addParamMap = new HashMap<>();
                            addParamMap.put("messageUuid", receiveMessageDto.getMessageUuid());
                            addParamMap.put("processStatus", receiveMessageDto.getProcessStatus().toString());
                            addParamMap.put("resultText", ackDto.getResultText());
                            dataMapList.forEach(map -> map.putAll(addParamMap));
                            sqlExecuteService.update(dataSourceId, postProcessingSqlId, dataMapList);
                        }
                        if(agentConfigReader.isConnectedConsole()) {
                            MessageUtils.announceMessageHistory(this.agentConfigReader, receiveMessageDto);
                        }
                    }
                }
                catch (JsonProcessingException e) {
                    log.warn("대상 agent {}[{}]에게 전송하였으나 응답메시지가 비정상입니다. 응답메시지: {}", knownAgent.getHostId(), knownAgent.getHostName(), response, e);
                }
                catch (ResourceAccessException e) {
                    log.warn("대상 agent {}[{}]에게 전송하였으나 응답하지 않습니다. exception: {}", knownAgent.getHostId(), knownAgent.getHostName(), e.getLocalizedMessage());
                    agentConfigReader.setKnownAgentStatus(knownAgent.getHostId(), "DOWN");
                }
            }
            else if(AgentConfigDto.TaskType.FILE_READ_THEN_SEND == postman.getTaskType()) {
                File rootDirectoryfile = new File(postman.getMessage().getWatchDirectory());
                SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd/hh/mm/ss");
                Date today = new Date(System.currentTimeMillis());
                String dateString = simpleDateFormat.format(today);
                List<File> fileReadyList = new ArrayList<>();
                List<File> workingFileList = new ArrayList<>();
                if(rootDirectoryfile.isDirectory() && rootDirectoryfile.listFiles().length > 0) {
                    // 대상 파일 찾기 -> 작업 디렉토리(working)로 이동 -> 작업 완료 후 완료 디렉토리(done)로 이동
                    File[] files = rootDirectoryfile.listFiles();
                    for(File file : files) {
                        if(file.isFile()) {
                            fileReadyList.add(file);
                        }
                    }
                    List<Map<String, Object>> data = new ArrayList<>();
                    String dataString = null;
                    String workingDirectory = rootDirectoryfile.getAbsolutePath() + "/working/" + dateString;
                    File workingDirectoryFile = new File(workingDirectory);
                    if(!workingDirectoryFile.exists()) {
                        workingDirectoryFile.mkdirs();
                    }
                    try {
                        // 작업 디렉토리(working)로 이동
                        for(File file : fileReadyList) {
                            Files.move(file.toPath(), workingDirectoryFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
                            workingFileList.add(new File(workingDirectoryFile.getAbsoluteFile() + "/" + file.getName()));
                        }
                        // ReceiveMessageDto - messageJson 만들기
                        for(File file : workingFileList) {
                            Map<String, Object> map = new HashMap<>();
                            Map<String, Object> paramMap = new HashMap<>();
                            map.put("file-name", file.getName());
                            if(postman.getMessage() != null && StringUtils.isNotEmpty(postman.getMessage().getMetaDropBoxId())
                                    && StringUtils.isNotEmpty(postman.getMessage().getMetaDataSqlId()) && StringUtils.isNotEmpty(postman.getMessage().getMetaDataDataSourceId())) {
                                map.put("meta-drop-box-id", postman.getMessage().getMetaDropBoxId());
                                paramMap.put("fileName", file.getName());
                                List<Map<String, Object>> messageDataMapList = sqlExecuteService.select(postman.getMessage().getDataSourceId(), postman.getMessage().getMetaDataSqlId(), paramMap);
                                if(messageDataMapList != null && messageDataMapList.get(0) != null) {
                                    map.put("meta-data", messageDataMapList.get(0));
                                }
                            }
                            data.add(map);
                        }
                        dataString = objectMapper.writeValueAsString(data);
                        ReceiveMessageDto receiveMessageDto = ReceiveMessageDto.builder().senderHostId(senderHostId).senderTimestamp(senderTimestamp)
                                .messageUuid(messageUuid).messageType(AgentConfigDto.MessageType.TRANSFER_DB_TO_DB)
                                .recipientHostId(postman.getRecipientHostId()).recipientDropBoxId(postman.getRecipientDropBoxId())
                                .data(dataString).build();
                        this.writeSentMessage(receiveMessageDto);
                        String messageString = objectMapper.writeValueAsString(receiveMessageDto);
                        // http 준비
                        HttpHeaders httpHeaders = new HttpHeaders();
                        httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
                        httpHeaders.set("User-Agent", agentConfigReader.getApplicationName() + ", version: " + agentConfigReader.getApplicationVersion() + "(" + agentConfigReader.getApplicationCommitId() + ")"
                                + ", host ID: " + agentConfigReader.getAgentConfigDto().getMyHostId()
                                + ", action: postman(postman-id: " + postman.getPostmanId() + ")"
                        );
                        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
                        // 첫 번째 멀티파트는 message json
                        body.add("json", new HttpEntity<>(messageString, httpHeaders));
                        // 두 번째 이후 멀티파트는 파일
                        for(File file : workingFileList) {
                            body.add(file.getName(), new FileSystemResource(file));
                        }
                        // 전송
                        RestTemplate restTemplate = new RestTemplate();
                        String url = "https://" + knownAgent.getHostName() + ":" + knownAgent.getListenPort() + "/listen";
                        String response = restTemplate.postForObject(url, body, String.class);
                        AckDto ackDto = objectMapper.readValue(response, new TypeReference<AckDto>() {});
                        if(AckDto.ResultType.RECEIVE_SUCCESS != ackDto.getResult()) {
                            throw new DfxException(postman.getRecipientHostId() + "에게 전송하였으나 상대방이 수신하지 못하였습니다." + response);
                        }
                        else {
                            // 작업 완료(done) 디렉토리로 이동
                            String doneDirectory = rootDirectoryfile + "/done/" + dateString;
                            File doneDirectoryFile = new File(doneDirectory);
                            if(!doneDirectoryFile.exists()) {
                                doneDirectoryFile.mkdirs();
                            }
                            for(File file : workingFileList) {
                                Files.move(file.toPath(), workingDirectoryFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
                            }
                        }
                    } catch (JsonProcessingException e) {
                        throw new RuntimeException(e);
                    } catch (DfxException e) {
                        throw new RuntimeException(e);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        else {
            log.warn("대상 agent {}[{}] 실행 상태가 정상(ALIVE)이 아니므로 전송을 중지합니다. postman ID: {}", knownAgent.getHostId(), knownAgent.getHostName(), postman.getPostmanId());
        }
    }

    public void transferReceiveMessageDto(ReceiveMessageDto receiveMessageDto) {
        //List<Map<String, Object>> routingHostList = receiveMessageDto.getRoutingHostList();

    }
}
