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


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tsi.sig.server.config.KafkaConsumerConfig;
import com.tsi.sig.server.websocket.OneTopicCvimHandler;
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.PartitionInfo;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.WakeupException;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

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

    private AtomicBoolean stopFlag = new AtomicBoolean(true);
    private AtomicBoolean updateTopics = new AtomicBoolean(false);

    private List<TopicPartition> topicPartitions;
    private List<Map<String, Object>> signalData;
    private final List<Map<String, Object>> sessionNodeId;
    private KafkaConsumer<String, ByteBuffer> consumer;
    private Map<String, List<PartitionInfo>> consumerTopicMap;
    private final List<String> nodeTopics;

    private final Set<WebSocketSession> sessionSet = new HashSet<>();
    private final OneTopicCvimHandler websocketHandler;
    private final KafkaConsumerConfig kafkaConsumerConfig;
    private ObjectMapper mapper;

    public OneTopicConsumerThread(OneTopicCvimHandler websocketHandler, KafkaConsumerConfig kafkaConsumerConfig) {
        this.websocketHandler = websocketHandler;
        this.kafkaConsumerConfig = kafkaConsumerConfig;
        this.signalData = new ArrayList<Map<String, Object>>();
        this.sessionNodeId = new ArrayList<Map<String, Object>>();
        this.topicPartitions = Collections.synchronizedList(new ArrayList<TopicPartition>());
        this.consumer = new KafkaConsumer<>(this.kafkaConsumerConfig.getConsumerNodeProperties());
        this.consumerTopicMap = this.consumer.listTopics();
        this.nodeTopics = new CopyOnWriteArrayList();
        this.mapper = new ObjectMapper();
    }

    public void addNodeSession(WebSocketSession session, String nodeId) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("session", session);
        map.put("nodeId", nodeId);

        synchronized (this.sessionNodeId) {
            this.sessionNodeId.add(map);
        }

        synchronized (this.nodeTopics) {
            boolean found = false;
            for (String id : this.nodeTopics) {
                if (id.equals(nodeId)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                // 신규로 요청한 노드인 경우
                this.nodeTopics.add(nodeId);
                this.updateTopics.set(true);
            }
        }
    }

    public void removeNodeSession(String sessionId) {
        String nodeId ="";
        try {
            synchronized (this.sessionNodeId) {
                for (Map<String, Object> map : this.sessionNodeId) {
                    if (((WebSocketSession) map.get("session")).getId().equals(sessionId)) {
                        nodeId = map.get("nodeId").toString();
                        this.sessionNodeId.remove(map);
                        break;
                    }
                }
            }
            boolean found = false;
            for (Map<String, Object> map : sessionNodeId) {
                if(map.get("nodeId").toString().equals(nodeId)){
                    found = true;
                    break;
                }
            }

            if (!nodeId.equals("") && !found) {
                // 다른 요청이 존재하지 않는 노드인 경우 삭제
                synchronized (this.nodeTopics) {
                    this.nodeTopics.remove(nodeId);
                }
                this.updateTopics.set(true);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // offset 구하기
    public long getOffset(String topic, int partition, boolean forceFromStart) {
        TopicPartition topicAndPartition = new TopicPartition(topic, partition);
        Map<TopicPartition, Long> offsetMap = null;
        if (forceFromStart) {
            offsetMap = this.consumer.beginningOffsets(Arrays.asList(topicAndPartition));
        } else {
            offsetMap = this.consumer.endOffsets(Arrays.asList(topicAndPartition));
        }
        if (offsetMap.get(topicAndPartition) != null) {
            return offsetMap.get(topicAndPartition);
        } else {
            return -1;
        }
    }

    private void SetDataMapONE(String key, ByteBuffer value, int valueSize) {
        Map<String, Object> header = new HashMap<>();
        byte info = value.get(4);
        byte comm = value.get(5);
        int counter = value.get(6) & 0xff;                    //주기카운터
        int sttsCount = value.get(7) & 0x7F;           //신호정보개수
        int divFlag = (value.get(7) >> 7) & 0x01;      //분활flag

        int oprTrans = (info >> 4) & 0x01;            //전이
        int oprInd = (info >> 3) & 0x01;              //감응
        int oprTurnoff = (info >> 2) & 0x01;          //소등
        int oprBlink = (info >> 1) & 0x01;            //점멸
        int oprManual = (info) & 0x01;                //수동

        int errScu = (comm >> 2) & 0x01;            //scu상태
        int errCenter = (comm >> 1) & 0x01;         //센터상태
        int errCont = (comm) & 0x01;                //모순상태

        Date date = (Date) new Date(((long) value.getInt(8) & 0xffffffffL) * 1000L);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdf.setTimeZone(TimeZone.getTimeZone("GMT+9"));
        String formattedDate = sdf.format(date);

        header.put("nodeId", key);
        header.put("oprTrans", oprTrans);
        header.put("oprInd", oprInd);
        header.put("oprTurnoff", oprTurnoff);
        header.put("oprBlink", oprBlink);
        header.put("oprManual", oprManual);
        header.put("errScu", errScu);
        header.put("errCenter", errCenter);
        header.put("errCont", errCont);
        header.put("date", formattedDate);
        header.put("dataCount", sttsCount);
        header.put("counter", counter);

        this.signalData.add(header);

        int idx = 0;
        for (int ii = 0; ii < sttsCount; ii++) {
            idx = ii * 5;

            int light = (value.get(12 + idx) >> 4) & 0x0F;            // 신호등정보 [직진,좌,보행]
            int dirAdd = (value.get(12 + idx)) & 0x0F;                //연등지

            int timeFlag = (value.get(13 + idx) >> 7) & 0x01;         //시간정보신뢰성
            int walker = (value.get(13 + idx) >> 6) & 0x01;           //보행자
            int unprotected = (value.get(13 + idx) >> 3) & 0x01;      //비보호 상태
            int stts = (value.get(13 + idx)) & 0x07;                  //신호등상태

            int dispTm = value.get(14 + idx) & 0xff;                  //표출시간
            int remainTm = value.get(15 + idx) & 0xff;                //잔여시간
            int dirCode = value.get(16 + idx);                        //방향코드
            Map<String, Object> temp = new HashMap<>();
            temp.put("light", light);
            temp.put("dirAdd", dirAdd);
            temp.put("timeFlag", timeFlag);
            temp.put("walker", walker);
            temp.put("unprotected", unprotected);
            temp.put("stts", stts);
            temp.put("dispTm", dispTm);
            temp.put("remainTm", remainTm);
            temp.put("dirCode", dirCode);

            this.signalData.add(temp);
        }
    }

    private void testData() {
        Map<String, Object> header = new HashMap<>();

        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdf.setTimeZone(TimeZone.getTimeZone("GMT+9"));
        String formattedDate = sdf.format(date);
        header.put("nodeId", 110000001);
        header.put("oprTrans", 0);
        header.put("oprInd", 0);
        header.put("oprTurnoff", 0);
        header.put("oprBlink", 0);
        header.put("oprManual", 0);
        header.put("errScu", 0);
        header.put("errCenter", 0);
        header.put("errCont", 0);
        header.put("date", formattedDate);
        header.put("dataCount", 16);
        header.put("counter", 43);

        this.signalData.add(header);

        int[] light = {1, 2, 3, 7, 1, 2, 3, 7, 1, 2, 3, 7, 1, 2, 3, 7};        // 신호등정보 [직진,좌,보행,유턴]
        //int[]  light  	=  {1,2,3,1,2,3,1,2,3,1,2,3};		// 신호등정보 [직진,좌,보행]
        int[] dirAdd = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};            //연등지

        int[] timeFlag = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};            //시간정보신뢰성
        int[] walker = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};        //보행자
        int[] unprotected = {0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};            //비보호 상태
        int[] stts = {3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1};        //신호등상태

        int[] dispTm = {30, 30, 20, 20, 20, 40, 40, 20, 10, 10, 01, 10, 10, 10, 10, 10};                        //표출시간
        int[] remainTm = {0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 5, 20, 0, 0, 0, 0};                    //잔여시간
        int[] dirCode = {10, 10, 10, 10, 20, 20, 20, 20, 30, 30, 30, 30, 50, 50, 50, 50};                    //방향코드

        for (int ii = 0; ii < 16; ii++) {

            Map<String, Object> temp = new HashMap<>();
            temp.put("light", light[ii]);
            temp.put("dirAdd", dirAdd[ii]);
            temp.put("timeFlag", timeFlag[ii]);
            temp.put("walker", walker[ii]);
            temp.put("unprotected", unprotected[ii]);
            temp.put("stts", stts[ii]);
            temp.put("dispTm", dispTm[ii]);
            temp.put("remainTm", remainTm[ii]);
            temp.put("dirCode", dirCode[ii]);

            signalData.add(temp);
        }
    }

    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("OneTopicConsumerThread, Start consumer: {}", this.getClass().getSimpleName());

        this.stopFlag.set(false);

        try {
            while (!this.stopFlag.get() && (!Thread.currentThread().isInterrupted())) {
                if (this.nodeTopics.size() == 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }
                if (this.consumer == null) {
                    this.consumer = new KafkaConsumer<>(this.kafkaConsumerConfig.getConsumerNodeProperties());
                }

                if (this.updateTopics.get()) {
                    this.updateTopics.set(false);
                    this.consumer.subscribe(this.nodeTopics,
                            new ConsumerRebalanceListener() {
                                @Override
                                public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
                                    log.info("OneTopicConsumerThread, onPartitionsRevoked - partitions: {}", formatPartitions(partitions));
                                }

                                @Override
                                public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
                                    log.info("OneTopicConsumerThread, onPartitionsAssigned - partitions: {}", formatPartitions(partitions));
                                    consumer.seekToEnd(partitions);
                                }
                            });
                }

                ConsumerRecords<String, ByteBuffer> records = this.consumer.poll(Duration.ofMillis(100));
                for (ConsumerRecord<String, ByteBuffer> record : records) {
                    SetDataMapONE(record.key().toString(), record.value(), record.serializedValueSize());
                    if (this.signalData.size() > 0) {
                        try {
                            String jsonInString = this.mapper.writeValueAsString(this.signalData);
                            synchronized (this.sessionNodeId) {
                                for (Map<String, Object> map : this.sessionNodeId) {
                                    if (this.signalData.get(0).get("nodeId").toString().equals(map.get("nodeId").toString())) {
                                        WebSocketSession session = (WebSocketSession) map.get("session");
                                        try {
                                            this.websocketHandler.sendMessage(session, new TextMessage(jsonInString));
                                            log.info("OneTopicConsumerThread, Send to: {}, {}, {} bytes.", session.getRemoteAddress().getAddress(), record.key(), jsonInString.length());
                                        } catch (Exception e) {
                                            log.error("OneTopicConsumerThread, Send Failed: {}, {}, {} bytes.", session, record.key(), jsonInString.length());
                                        }
                                    }
                                }
                            }
                        }
                        catch(JsonProcessingException e) {
                            log.error("OneTopicConsumerThread, ConsumerThread Json parsing Exception: {}", e.getMessage());
                        }
                    }
                    this.signalData.clear();
                }
            }
            log.info("OneTopicConsumerThread, ConsumerThread: {}, stopped.", this.getClass().getSimpleName());
        }
        catch(WakeupException e) {
            log.error("OneTopicConsumerThread, Consumer WakeupException: {}, {}", this.getClass().getSimpleName(), e);
            stop();
        }
        finally {
            //this.consumer.commitSync();
            stop();
            try {
                this.consumer.close();
            } catch(Exception e) {}
        }
    }

    public void stop() {
        this.stopFlag.set(true);
    }

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

