package com.bsmlab.dfx.agent.support;

import com.bsmlab.dfx.agent.config.AgentConfigDto;
import com.bsmlab.dfx.agent.config.AgentConfigReader;
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.bsmlab.dfx.agent.support.exception.IllegalMessageException;
import com.bsmlab.dfx.agent.support.exception.InCompleteMessageException;
import com.bsmlab.dfx.agent.support.exception.NullMessageException;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonFactoryBuilder;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;

import java.util.*;

@Slf4j
public class MessageUtils {
    private static final ObjectMapper objectMapper;
    static {
        StreamReadConstraints constraints = StreamReadConstraints.builder().maxStringLength(500_000_000).build();
        JsonFactory jsonFactory = new JsonFactoryBuilder().streamReadConstraints(constraints).build();
        objectMapper = new ObjectMapper(jsonFactory);
    }

    private MessageUtils() {};

    @SuppressWarnings("unchecked")
    public static ReceiveMessageDto toReceiveMessageDto(String messageJsonString) throws IllegalMessageException, NullMessageException, InCompleteMessageException {
        ReceiveMessageDto receiveMessageDto = null;
        Map<String, Object> map = null;
        try {
            map = objectMapper.readValue(messageJsonString, new TypeReference<HashMap<String, Object>>() {});
            if(map == null) {
                throw new NullMessageException("");
            }
            String senderHostId;
            if(map.get("senderHostId") == null) {
                throw new InCompleteMessageException("senderHostId 엘리먼트를 찾을 수 없습니다.");
            }
            else if(StringUtils.isBlank(String.valueOf(map.get("senderHostId")))) {
                throw new InCompleteMessageException("senderHostId 값을 찾을 수 없습니다.");
            }
            else {
                senderHostId = String.valueOf(map.get("senderHostId"));
            }
            long senderTimestamp = 0;
            if(map.get("senderTimestamp") == null) {
                throw new InCompleteMessageException("senderTimestamp 엘리먼트를 찾을 수 없습니다.");
            }
            else if(StringUtils.isBlank(String.valueOf(map.get("senderTimestamp")))) {
                throw new InCompleteMessageException("senderTimestamp 값을 찾을 수 없습니다.");
            }
            else {
                String senderTimeStampString = String.valueOf(map.get("senderTimestamp"));
                try {
                    senderTimestamp = Long.parseLong(senderTimeStampString);
                } catch (NumberFormatException e) {
                    throw new InCompleteMessageException("senderTimestamp 값의 형식이 숫자형식이 아닙니다. " + senderTimeStampString);
                }
            }
            String senderPostmanId;
            if(map.get("senderPostmanId") == null) {
                throw new InCompleteMessageException("senderPostmanId 엘리먼트를 찾을 수 없습니다.");
            }
            else if(StringUtils.isBlank(String.valueOf(map.get("senderPostmanId")))) {
                throw new InCompleteMessageException("senderPostmanId 값을 찾을 수 없습니다.");
            }
            else {
                senderPostmanId = String.valueOf(map.get("senderPostmanId"));
            }
            String messageUuid;
            if(map.get("messageUuid") == null) {
                throw new InCompleteMessageException("messageUuid 엘리먼트를 찾을 수 없습니다.");
            }
            else if(StringUtils.isBlank(String.valueOf(map.get("messageUuid")))) {
                throw new InCompleteMessageException("messageUuid 값을 찾을 수 없습니다.");
            }
            else {
                messageUuid = String.valueOf(map.get("messageUuid"));
                try {
                    UUID.fromString(messageUuid);
                }
                catch (IllegalArgumentException e) {
                    throw new InCompleteMessageException("messageUuid 값의 형식이 숫자형식이 아닙니다. " + messageUuid);
                }
            }
            AgentConfigDto.MessageType messageType;
            if(map.get("messageType") == null) {
                throw new InCompleteMessageException("messageType 엘리먼트를 찾을 수 없습니다.");
            }
            else if(StringUtils.isBlank(String.valueOf(map.get("messageType")))) {
                throw new InCompleteMessageException("messageType 값을 찾을 수 없습니다.");
            }
            else {
                String messageTypeString = String.valueOf(map.get("messageType"));
                if(!EnumUtils.isValidEnum(AgentConfigDto.MessageType.class, messageTypeString)) {
                    throw new InCompleteMessageException("messageType 값이 옳바르지 않습니다. " + messageTypeString);
                }
                messageType = EnumUtils.getEnum(AgentConfigDto.MessageType.class, messageTypeString);
            }
            long receivedTimestamp = 0;
            if(map.get("receivedTimestamp") == null) {
                throw new InCompleteMessageException("receivedTimestamp 엘리먼트를 찾을 수 없습니다.");
            }
            else if(StringUtils.isBlank(String.valueOf(map.get("receivedTimestamp")))) {
                throw new InCompleteMessageException("receivedTimestamp 값을 찾을 수 없습니다.");
            }
            else {
                String receivedTimestampString = String.valueOf(map.get("receivedTimestamp"));
                try {
                    receivedTimestamp = Long.parseLong(receivedTimestampString);
                } catch (NumberFormatException e) {
                    throw new InCompleteMessageException("receivedTimestamp 값의 형식이 숫자형식이 아닙니다. " + receivedTimestampString);
                }
            }
            String recipientHostId;
            if(map.get("recipientHostId") == null) {
                throw new InCompleteMessageException("recipientHostId 엘리먼트를 찾을 수 없습니다.");
            }
            else if(StringUtils.isBlank(String.valueOf(map.get("recipientHostId")))) {
                throw new InCompleteMessageException("recipientHostId 값을 찾을 수 없습니다.");
            }
            else {
                recipientHostId = String.valueOf(map.get("recipientHostId"));
            }
            String recipientDropBoxId;
            if(map.get("recipientDropBoxId") == null) {
                throw new InCompleteMessageException("recipientDropBoxId 엘리먼트를 찾을 수 없습니다.");
            }
            else if(StringUtils.isBlank(String.valueOf(map.get("recipientDropBoxId")))) {
                throw new InCompleteMessageException("recipientDropBoxId 값을 찾을 수 없습니다.");
            }
            else {
                recipientDropBoxId = String.valueOf(map.get("recipientDropBoxId"));
            }
            List<ReceiveMessageDto.RoutingHost> routingHostList = null;
            if(map.get("routingHostList") == null) {
                throw new InCompleteMessageException("routingHostList 엘리먼트를 찾을 수 없습니다.");
            }
            else {
                List<Map<String, Object>> mapList = (List<Map<String, Object>>) map.get("routingHostList");
                routingHostList = new ArrayList<>();
                for(Map<String, Object> map1 : mapList) {
                    ReceiveMessageDto.RoutingHost routingHost = ReceiveMessageDto.RoutingHost.builder().hostId(String.valueOf(map1.get("hostId"))).receivedTimestamp(Long.parseLong(String.valueOf(map1.get("receivedTimestamp")))).build();
                    routingHostList.add(routingHost);
                }
            }
            String dataString;
            if(map.get("data") == null) {
                throw new InCompleteMessageException("data 엘리먼트를 찾을 수 없습니다.");
            }
            else if(!(map.get("data") instanceof String)) {
                throw new InCompleteMessageException("data 엘리먼트의 데이터가 문자 타입이 아닙니다.");
            }
            else {
                dataString = String.valueOf(map.get("data"));
            }
            ReceiveMessageDto.ProcessStatus processStatus;
            if(map.get("processStatus") == null) {
                throw new InCompleteMessageException("processStatus 엘리먼트를 찾을 수 없습니다.");
            }
            else if(StringUtils.isBlank(String.valueOf(map.get("processStatus")))) {
                throw new InCompleteMessageException("processStatus 값을 찾을 수 없습니다.");
            }
            else {
                processStatus = EnumUtils.getEnum(ReceiveMessageDto.ProcessStatus.class, String.valueOf(map.get("processStatus")));
            }
            receiveMessageDto = ReceiveMessageDto.builder()
                    .senderHostId(senderHostId).senderTimestamp(senderTimestamp).senderPostmanId(senderPostmanId)
                    .messageUuid(messageUuid).messageType(messageType).receivedTimestamp(receivedTimestamp)
                    .recipientHostId(recipientHostId).recipientDropBoxId(recipientDropBoxId)
                    .routingHostList(routingHostList)
                    .data(dataString).attachFileList(new ArrayList<>()).processStatus(processStatus)
                    .build();
        }
        catch(JsonProcessingException e) {
            throw new IllegalMessageException(e.getMessage());
        }
        return receiveMessageDto;
    }

    public static AckDto toAckDto(String messageJsonString) throws NullMessageException, IllegalMessageException {
        Map<String, Object> map = null;
        AckDto ackDto;
        try {
            map = objectMapper.readValue(messageJsonString, new TypeReference<HashMap<String, Object>>() {});
            if(map == null) {
                throw new NullMessageException("");
            }
            else {
                AckDto.ResultType result = EnumUtils.getEnum(AckDto.ResultType.class, String.valueOf(map.get("result")));
                String resultText = String.valueOf(map.get("resultText"));
                String messageUuid = String.valueOf(map.get("messageUuid"));
                ackDto = AckDto.builder().result(result).resultText(resultText).messageUuid(messageUuid).build();
            }
        }
        catch(JsonProcessingException e) {
            throw new IllegalMessageException(e.getMessage());
        }
        return ackDto;
    }

    public static CommandDto toCommandDto(String messageJsonString) throws IllegalMessageException, NullMessageException {
        CommandDto commandDto;
        Map<String, Object> map = null;
        try {
            map = objectMapper.readValue(messageJsonString, new TypeReference<Map<String, Object>>() {});
            if(map == null) {
                throw new NullMessageException("command message json string is invalid");
            }
            else {
                CommandDto.CommandType commandType = EnumUtils.getEnum(CommandDto.CommandType.class, String.valueOf(map.get("commandType")));
                String messageUuid = String.valueOf(map.get("messageUuid"));
                commandDto = CommandDto.builder().commandType(commandType).messageUuid(messageUuid).build();
            }
        } catch (JsonProcessingException e) {
            throw new IllegalMessageException(e.getMessage());
        }
        return commandDto;
    }

    public static AckDto send(AgentConfigReader agentConfigReader, String postmanId, ReceiveMessageDto receiveMessageDto) throws JsonProcessingException {
        AgentConfigDto.KnownAgent recipientAgent = agentConfigReader.getKnownAgent(receiveMessageDto.getRecipientHostId());
        String targetHostId = recipientAgent.getRoutingHostIdList().get(0);
        AgentConfigDto.KnownAgent knownAgent = agentConfigReader.getKnownAgent(targetHostId);
        String protocol = knownAgent.isSslEnabled() ? "https://" : "http://";
        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: postman(postman-id: " + postmanId + ")"
        );
        HttpEntity<ReceiveMessageDto> bodyEntity = new HttpEntity<>(receiveMessageDto, httpHeaders);
        RestTemplate restTemplate = new RestTemplate();
        String url = protocol + knownAgent.getHostName() + ":" + knownAgent.getListenPort() + "/listen";
        String response = restTemplate.postForObject(url, bodyEntity, String.class);
        AckDto ackDto = objectMapper.readValue(response, new TypeReference<AckDto>() {});
        return ackDto;
    }

    public static AckDto transfer(AgentConfigReader agentConfigReader, ReceiveMessageDto receiveMessageDto) throws JsonProcessingException {
        String targetHostId = "";
        List<ReceiveMessageDto.RoutingHost> routingHostList = receiveMessageDto.getRoutingHostList();
        Iterator<ReceiveMessageDto.RoutingHost> iterator = routingHostList.iterator();
        while(iterator.hasNext()) {
            ReceiveMessageDto.RoutingHost routingHost = iterator.next();
            if(agentConfigReader.getAgentConfigDto().getMyHostId().equals(routingHost.getHostId())) {
                ReceiveMessageDto.RoutingHost nextRoutingHost = iterator.next();
                targetHostId = nextRoutingHost.getHostId();
                routingHost.setReceivedTimestamp(System.currentTimeMillis());
                break;
            }
        }
        AgentConfigDto.KnownAgent knownAgent = agentConfigReader.getKnownAgent(targetHostId);
        String protocol = knownAgent.isSslEnabled() ? "https://" : "http://";
        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: transfer(next-host-id: " + targetHostId + ")"
        );
        HttpEntity<ReceiveMessageDto> bodyEntity = new HttpEntity<>(receiveMessageDto, httpHeaders);
        RestTemplate restTemplate = new RestTemplate();
        String url = protocol + knownAgent.getHostName() + ":" + knownAgent.getListenPort() + "/listen";
        String response = restTemplate.postForObject(url, bodyEntity, String.class);
        AckDto ackDto = objectMapper.readValue(response, new TypeReference<AckDto>() {});
        return ackDto;
    }

    public static AckDto alive(AgentConfigReader agentConfigReader, AgentConfigDto.KnownAgent knownAgent) throws JsonProcessingException {
        String messageUuid = UUID.randomUUID().toString();
        CommandDto commandDto = CommandDto.builder().commandType(CommandDto.CommandType.ALIVE).messageUuid(messageUuid).build();
        String protocol = knownAgent.isSslEnabled() ? "https://" : "http://";
        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: status-check(ALIVE)"
        );
        HttpEntity<CommandDto> bodyEntity = new HttpEntity<>(commandDto, httpHeaders);
        RestTemplate restTemplate = new RestTemplate();
        String url = protocol + knownAgent.getHostName() + ":" + knownAgent.getListenPort() + "/command";
        log.debug("StatusChecker to {} send a message UUID {}", knownAgent.getHostName(), messageUuid);
        String response = "";
        AckDto ackDto = null;
//        try {
            response = restTemplate.postForObject(url, bodyEntity, String.class);
//        }
//        catch(Exception e) {
//            log.error("send a alive message: response = restTemplate.postForObject(url, bodyEntity, String.class);", e);
//        }
        ackDto = objectMapper.readValue(response, new TypeReference<AckDto>() {});
        return ackDto;
    }

    public static AckDto announceInformation(AgentConfigReader agentConfigReader) throws JsonProcessingException {
        String messageUuid = UUID.randomUUID().toString();
        String data = objectMapper.writeValueAsString(agentConfigReader.getAgentConfigDto());
        CommandDto commandDto = CommandDto.builder().commandType(CommandDto.CommandType.INFORMATION).messageUuid(messageUuid).data(data).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: announce-information"
        );
        HttpEntity<CommandDto> bodyEntity = new HttpEntity<>(commandDto, httpHeaders);
        RestTemplate restTemplate = new RestTemplate();
        String url = (agentConfigReader.getAgentConfigDto().getStatusChecker().isConsoleSslEnabled() ? "https://" : "http://")
                + agentConfigReader.getAgentConfigDto().getStatusChecker().getConsoleHostName() + ":"
                + agentConfigReader.getAgentConfigDto().getStatusChecker().getConsoleListenPort()
                + "/command";
        log.debug("announceInformation to console {} send a message UUID {}", agentConfigReader.getAgentConfigDto().getStatusChecker().getConsoleHostName(), messageUuid);
        String response = "";
        AckDto ackDto = null;
        response = restTemplate.postForObject(url, bodyEntity, String.class);
        ackDto = objectMapper.readValue(response, new TypeReference<AckDto>() {});
        return ackDto;
    }

    public static AckDto announceMessageHistory(AgentConfigReader agentConfigReader, ReceiveMessageDto receiveMessageDto) throws JsonProcessingException {
        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: message-history"
        );
        HttpEntity<ReceiveMessageDto> bodyEntity = new HttpEntity<>(receiveMessageDto, httpHeaders);
        RestTemplate restTemplate = new RestTemplate();
        String url = (agentConfigReader.getAgentConfigDto().getStatusChecker().isConsoleSslEnabled() ? "https://" : "http://")
                + agentConfigReader.getAgentConfigDto().getStatusChecker().getConsoleHostName() + ":"
                + agentConfigReader.getAgentConfigDto().getStatusChecker().getConsoleListenPort()
                + "/listen";
        log.debug("announceMessageHistory to console {} send a message UUID {}", agentConfigReader.getAgentConfigDto().getStatusChecker().getConsoleHostName(), receiveMessageDto.getMessageUuid());
        String response = "";
        AckDto ackDto = null;
        response = restTemplate.postForObject(url, bodyEntity, String.class);
        ackDto = objectMapper.readValue(response, new TypeReference<AckDto>() {});
        return ackDto;
    }

    public static AckDto announceMessageAck(AgentConfigReader agentConfigReader, AckDto ackDto) throws JsonProcessingException {
        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: message-history"
        );
        HttpEntity<AckDto> bodyEntity = new HttpEntity<>(ackDto, httpHeaders);
        RestTemplate restTemplate = new RestTemplate();
        String url = (agentConfigReader.getAgentConfigDto().getStatusChecker().isConsoleSslEnabled() ? "https://" : "http://")
                + agentConfigReader.getAgentConfigDto().getStatusChecker().getConsoleHostName() + ":"
                + agentConfigReader.getAgentConfigDto().getStatusChecker().getConsoleListenPort()
                + "/telegram";
        log.debug("announceMessageAck to console {} send a message UUID {}. result: {}", agentConfigReader.getAgentConfigDto().getStatusChecker().getConsoleHostName(), ackDto.getMessageUuid(), ackDto.getResult());
        String response = "";
        AckDto consoleAckDto = null;
        response = restTemplate.postForObject(url, bodyEntity, String.class);
        consoleAckDto = objectMapper.readValue(response, new TypeReference<AckDto>() {});
        return consoleAckDto;
    }
}
