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

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.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.common.util.StringUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.EnumUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Service;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;

@Service
@RequiredArgsConstructor
@Slf4j
public class DropBoxSchedulerService {
    private final ThreadPoolTaskScheduler dropBoxProcessorThreadPoolTaskScheduler;
    private final AgentConfigReader agentConfigReader;
    private final SqlExecuteService sqlExecuteService;
    private final DropBoxService dropBoxService;
    private Map<Integer, ScheduledFuture<?>> scheduledFutureMap;
    private ScheduledFuture<?> manualMessageFileScannerScheduledFuture;
    /**
     * 수신한 메시지를 처리하는 쓰레드
     * DB 저장 로직과 파일 또는 파일 + 메타데이터 저장 로직으로 구성됨
     * @param messageFilePath 메지시 파일 절대 경로
     */
    @SuppressWarnings("unchecked")
    public void processDropBox(String messageFilePath) throws Exception {
        ReceiveMessageDto receiveMessageDto = ReceiveMessageDto.builder().build();
        ReceiveMessageDto.ProcessStatus processStatus;
        String processMessage = "";
        String messageUuid;
        Exception processException = null;
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(messageFilePath))) {
            receiveMessageDto = (ReceiveMessageDto)objectInputStream.readObject();
            messageUuid = receiveMessageDto.getMessageUuid();
            AgentConfigDto.DropBox dropBox = agentConfigReader.getDropBox(receiveMessageDto.getRecipientDropBoxId());
            log.info("process messageUuid:{} dropBoxId: {}", messageUuid, dropBox.getDropBoxId());
            //1. DB 저장 메시지 처리
            if(dropBox.getTaskType() == AgentConfigDto.TaskType.RECEIVE_DB_TO_DB_SAVE) {
                ObjectMapper objectMapper = new ObjectMapper();
                List<Map<String, Object>> dataMapList;
                dataMapList = (List<Map<String, Object>>) objectMapper.readValue(receiveMessageDto.getData(), List.class);
                sqlExecuteService.insert(dropBox.getDataSourceId(), dropBox.getSqlId(), dataMapList);
            }
            else if(dropBox.getTaskType() == AgentConfigDto.TaskType.RECEIVE_FILE) {
                // 1.2 파일 수신 메시지 처리
                // 파일은 ListenService 에서 이미 저장하였음. meta-data 가 있는 경우 DB 저장
                ObjectMapper objectMapper = new ObjectMapper();
                List<Map<String, Object>> dataMapList;
                dataMapList = (List<Map<String, Object>>) objectMapper.readValue(receiveMessageDto.getData(), List.class);
                for(String attachFilePath : receiveMessageDto.getAttachFileList()) {
                    String fileName = attachFilePath.substring(attachFilePath.lastIndexOf("/") + 1);
                    Map<String, Object> dataMap = null;
                    for(Map<String, Object> map : dataMapList) {
                        if(String.valueOf(map.get("file-name")).equals(fileName)) {
                            dataMap = map;
                            break;
                        }
                    }
                    if(dataMap != null && StringUtils.isNotBlank(String.valueOf(dataMap.get("meta-drop-box-id")))) {
                        AgentConfigDto.DropBox fileMetaDropBox = agentConfigReader.getDropBox(String.valueOf(dataMap.get("meta-drop-box-id")));
                        if(dataMap.get("meta-data") != null && dataMap.get("meta-data") instanceof Map) {
                            Map<String, Object> fileMetaData = (Map<String, Object>)dataMap.get("meta-data");
                            sqlExecuteService.insert(fileMetaDropBox.getDataSourceId(), fileMetaDropBox.getSqlId(), fileMetaData);
                        }
                    }
                }
            }
            processStatus = ReceiveMessageDto.ProcessStatus.PROCESS_DONE;
        } catch (FileNotFoundException e) {
            //수신한 메시지 파일을 알 수 없으므로 로그 남기고 종료
            dropBoxService.clearMessageFile(messageFilePath);
            processStatus = ReceiveMessageDto.ProcessStatus.PROCESS_NOT_POSSIBLE;
            processMessage = e.getMessage();
            log.error("process not possible - messageFilePath: {}", messageFilePath);
            log.error("{}", e, e);
            processException = e;
        } catch (IOException e) {
            //수신한 메시지를 로드하지 못했으니 메시지 처리 불가. ReceiveMessageDto 를 알 수 없으므로 로그 남기고 종료
            processStatus = ReceiveMessageDto.ProcessStatus.PROCESS_FAIL;
            processMessage = e.getMessage();
            log.error("cannot load a message file - messageFilePath: {}", messageFilePath);
            log.error("{}", e, e);
            processException = e;
        } catch (ClassNotFoundException e) {
            //ReceiveMessageDto 변환 실패. 로그 남기고 파일을 미처리 상태 경로로 옮기고 ReceiveMessageDto 를 알 수 없으므로 종료
            processStatus = ReceiveMessageDto.ProcessStatus.PROCESS_FAIL;
            processMessage = e.getMessage();
            log.error("cannot parse a message file - messageFilePath: {}", messageFilePath);
            log.error("{}", e, e);
            processException = e;
        } catch (Exception e) {
            //기타 메시지 처리 중 오류
            processStatus = ReceiveMessageDto.ProcessStatus.PROCESS_FAIL;
            processMessage = e.getMessage();
            log.error("cannot process a message file - messageFilePath: {}", messageFilePath);
            log.error("{}", e, e);
            processException = e;
        }
        //2. 결과 Ack 전송
        if(StringUtils.isNotBlank(receiveMessageDto.getMessageUuid()) && StringUtils.isNotBlank(receiveMessageDto.getSenderHostId())) {
            receiveMessageDto.setProcessStatus(processStatus);
            this.ackDropBoxProcessResult(receiveMessageDto, processMessage);
        }
        //2.1 콘솔에 연결되어 있는 경우 콘솔에도 메시지 전송
        if(this.agentConfigReader.isConnectedConsole()) {
            MessageUtils.announceMessageHistory(this.agentConfigReader, receiveMessageDto);
        }
        //3. 처리한 메시지 이동
        try {
            boolean isSuccess = ReceiveMessageDto.ProcessStatus.PROCESS_DONE == processStatus; // PROCESS_DONE 이면 success 경로로 옮기고 아니면 failure 경로로 옮김
            if(ReceiveMessageDto.ProcessStatus.PROCESS_NOT_POSSIBLE != processStatus) { // PROCESS_NOT_POSSIBLE 인 경우는 메시지 파일을 찾을 수 없으므로 옮길 수 없음
                dropBoxService.moveMessageFile(messageFilePath, isSuccess);
            }
        } catch (IOException ex) {
            log.error("{}", ex, ex);
            processException = ex;
        }
        if(processException != null) {
            throw processException;
        }
    }

    private void ackDropBoxProcessResult(ReceiveMessageDto receiveMessageDto, String processMessage) {
        AckDto.ResultType resultType = null;
        if(receiveMessageDto.getProcessStatus() == ReceiveMessageDto.ProcessStatus.PROCESS_DONE) {
            resultType = AckDto.ResultType.PROCESS_SUCCESS;
        }
        else if(receiveMessageDto.getProcessStatus() == ReceiveMessageDto.ProcessStatus.PROCESS_FAIL || receiveMessageDto.getProcessStatus() == ReceiveMessageDto.ProcessStatus.PROCESS_NOT_POSSIBLE) {
            resultType = AckDto.ResultType.PROCESS_FAIL;
        }
        AckDto ackDto = AckDto.builder().result(resultType).messageUuid(receiveMessageDto.getMessageUuid()).resultText(processMessage).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: dropbox-ack(recipient-dropbox-id: " + receiveMessageDto.getRecipientDropBoxId() + ", message-uuid: " + receiveMessageDto.getMessageUuid() + ")"
        );
        HttpEntity<AckDto> bodyEntity = new HttpEntity<>(ackDto, httpHeaders);
        RestTemplate restTemplate = new RestTemplate();
        AgentConfigDto.KnownAgent knownAgent = agentConfigReader.getKnownAgent(receiveMessageDto.getSenderHostId());
        String url = "http://" + knownAgent.getHostName() + ":" + knownAgent.getListenPort() + "/telegram";
        ObjectMapper objectMapper = new ObjectMapper();
        AckDto responseAckDto;
        try {
            String response = restTemplate.postForObject(url, bodyEntity, String.class);
            responseAckDto = objectMapper.readValue(response, AckDto.class);
            log.info("message processing is done. {}", receiveMessageDto.getMessageUuid());
            log.info("response {}", responseAckDto);
        }
        catch (ResourceAccessException e) {
            log.error("Exception on sending ACK for dropbox process result. messageUuid: {}, Sending URL: {}", receiveMessageDto.getMessageUuid(), url);
            log.error("{}", e, e);
        }
        catch (JsonProcessingException e) {
            log.error("Exception after sending ACK. messageUuid: {} ", receiveMessageDto.getMessageUuid());
            log.error("{}", e, e);
        }
    }

    public void launch() { // 실행확인됨
        log.debug("{} launch", this.getClass().getName());
        dropBoxService.allowToPoll();
        this.scheduledFutureMap = new HashMap<>();
        for(int i = 0; i < this.agentConfigReader.getAgentConfigDto().getDropBoxConfig().getThreadPoolSize(); i++) {
            this.scheduleTask(i);
        }
        // 수동 메지시 파일 스캐너 쓰레드 시작
        this.startManualMessageFileScanner();
    }

    // 전체 DropBox 중지
    public void stopAll() {
        // 수동 메시지 파일 스캐너 중지
        manualMessageFileScannerScheduledFuture.cancel(true);
        // 큐에서 수신 메시지(파일) 가져오기 중지
        dropBoxService.denyToPoll();
        // Dropbox 쓰레드 중지
        for(ScheduledFuture<?> scheduledFuture : scheduledFutureMap.values()) {
            if(scheduledFuture != null) {
                scheduledFuture.cancel(true);
            }
            scheduledFutureMap.clear();
        }
    }

    private void scheduleTask(int taskId) {
        Runnable task = () -> {
            String messageFilePath = dropBoxService.poll();
            try {
                if(StringUtils.isNotBlank(messageFilePath)) {
                    log.info("dropBoxProcessThread {} process a message {} (extra poll size: {})", Thread.currentThread().getName(), messageFilePath, dropBoxService.getPollSize());
                    this.processDropBox(messageFilePath);
                }
                Thread.sleep(10);
            }
            catch(InterruptedException e) {
                log.error("{}", e, e);
                Thread.currentThread().interrupt();
                this.rescheduleTask(taskId);
            }
            catch(Exception e) {
                log.error("Exception on processDropBox. messageFilePath: {}", messageFilePath, e);
                this.rescheduleTask(taskId);
            }
        };
        ScheduledFuture<?> future = dropBoxProcessorThreadPoolTaskScheduler.scheduleWithFixedDelay(task, Duration.ofMillis(10));
        this.scheduledFutureMap.put(taskId, future);
    }

    private void rescheduleTask(int taskId) {
        ScheduledFuture<?> future = this.scheduledFutureMap.get(taskId);
        if(future != null && !future.isCancelled()) {
            future.cancel(true);
            scheduleTask(taskId);
        }
    }

    private void startManualMessageFileScanner() {
        Runnable messageFileScannerTask = () -> {
            try {
                dropBoxService.scanManualMessageFile(100);
            } catch (IOException e) {
                log.error("Exception on dropbox.scanManualMessageFile", e);
            }
        };
        this.manualMessageFileScannerScheduledFuture = this.dropBoxProcessorThreadPoolTaskScheduler.scheduleWithFixedDelay(messageFileScannerTask, Duration.ofMillis(1000));
    }

}
