package com.tsi.sig.server.websocket.kafka;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tsi.sig.server.app.AppUtils;
import com.tsi.sig.server.config.KafkaEvpConsumerConfig;
import com.tsi.sig.server.dto.*;
import com.tsi.sig.server.mapper.MainMapper;
import com.tsi.sig.server.repository.ApplicationRepository;
import com.tsi.sig.server.websocket.EvpTopicHandler;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.WakeupException;
import org.apache.kafka.common.utils.ByteBufferInputStream;
import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

@Slf4j
@Getter
@Setter
public class EvpTopicConsumerThread implements Runnable {

    private AtomicBoolean stopFlag = new AtomicBoolean(true);

    private final Set<WebSocketSession> sessionSet = new HashSet<>();
    private final EvpTopicHandler websocketHandler;
    private final KafkaEvpConsumerConfig kafkaEvpConsumerConfig;
    private final ApplicationRepository repo;
    public static final String KAFKA_EVPS_SERVICE = "evps-service";
    public static final String KAFKA_EVPS_NODE = "evps-node";
    public static final String KAFKA_EVPS_SIGNAL = "evps-signal";
    public static final String KAFKA_EVPS_EVENT = "evps-event";
    public static final String KAFKA_EVPS_SERVICE_END = "evps-service-end";

    private List<Map<String, Object>> signalData = new ArrayList<Map<String, Object>>();
    private KafkaConsumer<String, ByteBuffer> consumer;
    private String topicName;
    private ObjectMapper mapper;
    private MainMapper mainMapper;

    public EvpTopicConsumerThread(EvpTopicHandler websocketHandler, KafkaEvpConsumerConfig kafkaConsumerConfig) {
        this.repo = (ApplicationRepository) AppUtils.getBean(ApplicationRepository.class);
        this.websocketHandler = websocketHandler;
        this.kafkaEvpConsumerConfig = kafkaConsumerConfig;
        this.consumer = new KafkaConsumer<>(this.kafkaEvpConsumerConfig.getConsumerProperties());
        this.topicName = this.kafkaEvpConsumerConfig.getTopic();
        this.mapper = new ObjectMapper();
        this.mainMapper = (MainMapper)AppUtils.getBean(MainMapper.class);
    }

    private void SetDataMapALL(String key, ByteBuffer value, int valueSize){
        InputStream is = new ByteBufferInputStream(value);

        try {
            if (KAFKA_EVPS_EVENT.equals(key)) {
                EvpsEventDto event =  this.mapper.readValue(is, EvpsEventDto.class);
                checkServiceInfo(event.getServiceId(), key);
                if (this.repo.getEvpDataMap().get(event.getServiceId()) == null) return;
                this.repo.getEvpDataMap().get(event.getServiceId()).setEvpsEvent(event);
                this.repo.getEvpDataMap().get(event.getServiceId()).setEventList();
            }
            else if (KAFKA_EVPS_SIGNAL.equals(key)) {
                EvpsSignalDto signal = mapper.readValue(is, EvpsSignalDto.class);
                checkServiceInfo(signal.getServiceId(), key);
                if (this.repo.getEvpDataMap().get(signal.getServiceId()) == null) return;
                this.repo.getEvpDataMap().get(signal.getServiceId()).setEvpsSignal(signal);
                this.repo.getEvpDataMap().get(signal.getServiceId()).setSignalList();;
            }
            else if (KAFKA_EVPS_NODE.equals(key)) {
                EvpsNodeDto node = mapper.readValue(is, EvpsNodeDto.class);
                checkServiceInfo(node.getServiceId(), key);
                if (this.repo.getEvpDataMap().get(node.getServiceId()) == null) return;
                this.repo.getEvpDataMap().get(node.getServiceId()).setEvpsNode(node);
                this.repo.getEvpDataMap().get(node.getServiceId()).setNodeList();
            }
            else if (KAFKA_EVPS_SERVICE.equals(key)) {
                EvpsServiceDto service = mapper.readValue(is, EvpsServiceDto.class);
                log.info("===============  EVP SERVICE START - ServiceId : {}, Time {}", service.getServiceId(), service.getClctDt());
                this.repo.getEvpDataMap().put(service.getServiceId(), new EvpsData());
                this.repo.getEvpDataMap().get(service.getServiceId()).setEvpsService(service);
                this.repo.getEvpDataMap().get(service.getServiceId()).setServiceVo();
            }
            else if (KAFKA_EVPS_SERVICE_END.equals(key)) {
                EvpsServiceEndDto serviceEnd = mapper.readValue(is, EvpsServiceEndDto.class);
                if (serviceEnd.getReason() == 2) {
                    if (this.repo.getEvpDataMap().get(serviceEnd.getServiceId()) != null) {
                        this.repo.getEvpDataMap().remove(serviceEnd.getServiceId());
                    }
                    log.info("===============  EVP SERVICE END - ServiceId : {}, Time {}", serviceEnd.getServiceId(), serviceEnd.getClctDt());
                }
            }
            else {
                log.error("Unknown Utic Evps Kafka Key: {}, {}", key, value);
            }
        }
        catch (IOException e) {
//            e.getStackTrace();
            log.error("EvpTopicConsumerThread SetDataMapALL Cause IOException");
        }
    }

    /**
     * 긴급차량 서비스 메모리 데이터가 있는지 체크
     * @param serviceId 긴급차량 서비스 ID
     * @param key Kafka key
     */
    public void checkServiceInfo(String serviceId, String key) {
        Map<String, Object> param = new HashMap<>();
        param.put("serviceId", serviceId);
        if (this.repo.getEvpDataMap().get(serviceId) == null) { //메모리에 데이터가 없는 경우 데이터베이스에서 조회한다.
            EvpsServiceDto dto = this.mainMapper.getEvpServiceDto(param);
            if (dto == null) return; //서비스 조회가 안된다면 리턴

            EvpsData evpsData = new EvpsData();
            dto.setRouteList(this.mainMapper.getEvpRouteDtoList(param)); // 해당 서비스의 ROUTE 정보 조회
            evpsData.setEvpsService(dto);
            evpsData.setServiceVo();
            this.repo.getEvpDataMap().put(serviceId, evpsData);
        }

        //NODE LIST 데이터가 아닌 경우 NODE LIST 조회해서 메모리에 추가 해준다.
        if (KAFKA_EVPS_NODE.equals(key) || this.repo.getEvpDataMap().get(serviceId).getEvpsNode() != null) return;

        List<NodeInfo> list = this.mainMapper.getEvpNodeDtoList(param);

        //NODE LIST 데이터가 있을 경우만 phaseList 생성
        if (list == null || list.size() == 0) return;

        for (NodeInfo node : list) {
            param.put("nodeId", node.getNodeId());
            param.put("seqNo", node.getSeqNo());
            node.setPhaseList(this.mainMapper.getEvpPhaseDtoList(param));
        }

        this.repo.getEvpDataMap().get(serviceId).getEvpsNode().setNodeList(list);
    }

    public List<String> formatPartitions(Collection<TopicPartition> partitions) {
        return partitions.stream().map(topicPartition ->
                        String.format("\ntopic: %s, partition: %s", topicPartition.topic(), topicPartition.partition()))
                .collect(Collectors.toList());
    }

    @Override
    public void run() {

        log.info("EvpTopicConsumerThread, Start consumer: {}", this.topicName);

        this.stopFlag.set(false);

        try {
            this.consumer.subscribe(Collections.singletonList(this.topicName),
                    new ConsumerRebalanceListener() {
                        @Override
                        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
                            log.info("EvpTopicConsumerThread, onPartitionsRevoked - consumerName: {}, partitions: {}", topicName, formatPartitions(partitions));
                        }

                        @Override
                        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
                            log.info("EvpTopicConsumerThread, onPartitionsAssigned - consumerName: {}, partitions: {}", topicName, formatPartitions(partitions));
                            consumer.seekToEnd(partitions);
                        }
                    });
            while (!this.stopFlag.get() && (!Thread.currentThread().isInterrupted())) {

                ConsumerRecords<String, ByteBuffer> records = this.consumer.poll(Duration.ofMillis(100));

                for (ConsumerRecord<String, ByteBuffer> record : records) {
                    SetDataMapALL(record.key(), record.value(), record.serializedValueSize());
                }
            }
            log.info("EvpTopicConsumerThread, ConsumerThread: {}, stopped.", this.topicName);
        }
        catch(WakeupException e) {
            log.error("EvpTopicConsumerThread, Consumer WakeupException: {}, {}", this.topicName, e);
            stop();
        }
        finally {
            //this.consumer.commitSync();
            stop();
            try {
                this.consumer.close();
            } catch(IllegalArgumentException e) {
                log.error("EvpTopicConsumerThread Close Failed");
            }
        }
    }

    public void addSession(WebSocketSession session) {
        synchronized (this.sessionSet) {
            this.sessionSet.add(session);
        }
    }

    public void removeSession(WebSocketSession session) {
        log.info("EvpTopicConsumerThread, remove sessions: {}", this.sessionSet.toString());
        synchronized (this.sessionSet) {
            this.sessionSet.remove(session);
        }
    }

    public void stop() {
        log.info("============ Kafka stop ============");
        if (this.repo.getEvpDataMap().size()  > 0) {
            this.repo.getEvpDataMap().clear();
        }
        this.stopFlag.set(true);
    }

    public void shutdown() {
        log.info("EvpTopicConsumerThread, shutdown wakeup: {}", this.getClass().getSimpleName());
        this.consumer.wakeup();
    }
}
