package com.tsi.comm.server.process.packet;

import com.tsi.app.common.app.AppUtils;
import com.tsi.app.common.cpu.enums.eOpCode;
import com.tsi.app.common.utils.TimeUtils;
import com.tsi.comm.server.kafka.KafkaProducerService;
import com.tsi.comm.server.mongo.repository.TsiNodeStatusRepository;
import com.tsi.comm.server.process.AbstractTsiCvimWorker;
import com.tsi.comm.server.process.logging.TsiCvimLoggingProcess;
import com.tsi.comm.server.protocol.AbstractTsiPacket;
import com.tsi.comm.server.protocol.TsiCpuDisconnected;
import com.tsi.comm.server.protocol.TsiCpuPacket;
import com.tsi.comm.server.vo.TsiNodeVo;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

@Slf4j
public class TsiCvimPacketWorker extends AbstractTsiCvimWorker implements Runnable {

    //private ConcurrentLinkedQueue<AbstractTsiPacket> DATA_QUEUE;
    private final LinkedBlockingQueue<AbstractTsiPacket> DATA_QUEUE;
    private final KafkaProducerService kafkaProducer;
    private final TsiCvimLoggingProcess loggingService;
    private TsiNodeStatusRepository tsiNodeStatusRepository;

    public TsiCvimPacketWorker(int idx, int qSize, KafkaProducerService kafkaProducer) {
        this.idx = idx;
        this.qSize = qSize;
        this.kafkaProducer = kafkaProducer;
        this.loggingService = (TsiCvimLoggingProcess) AppUtils.getBean(TsiCvimLoggingProcess.class);
        this.DATA_QUEUE = new LinkedBlockingQueue<>(qSize);
        try {
            this.tsiNodeStatusRepository = (TsiNodeStatusRepository) AppUtils.getBean(TsiNodeStatusRepository.class);
        }
        catch(Exception e) {
            this.tsiNodeStatusRepository = null;
            log.error("MongoDB: {}", e.getMessage());
        }
    }

    @Override
    public void run() {
        log.info("{} Start. QSIZE: {}", Thread.currentThread().getName(), this.qSize);
        while (true) {
            try {
                Object packet = this.DATA_QUEUE.take();
                if (packet != null) {
                    process(packet);
                }
                else {
                    Thread.yield();
                }
            }
            catch (Exception e) {
                log.error("Exception: {}", e.getMessage());
            }
        }
    }

    /*
     *  작업큐에 데이터 추가
     */
    public boolean add(Object object) {
        boolean offer = false;
        AbstractTsiPacket packet = (AbstractTsiPacket)object;
        try {
            //offer => full -> return
            //add   => full -> wait
            offer = this.DATA_QUEUE.offer(packet);
            if (!offer) {
                MDC.put("id", Long.toString(packet.getNodeId()));
                log.error("Packet Queue.offer: {}/{}, Queue Full: {} EA, {}, {}",
                        packet.getNodeId(), this.DATA_QUEUE.size(), this.qSize, TimeUtils.elapsedTime(packet.getRcv()), Thread.currentThread().getName());
                MDC.clear();
            }
        } catch (Exception e) {
            MDC.put("id", Long.toString(packet.getNodeId()));
            log.error("Packet Queue.offer: Exception: {}, {}, {}", packet.getNodeId(), Thread.currentThread().getName(), e.getMessage());
            MDC.clear();
        }
        return offer;
    }

    public void process(Object object) {
        AbstractTsiPacket packet = (AbstractTsiPacket)object;
        TsiCpuPacket cpuPacket = (TsiCpuPacket)packet;
        TsiNodeVo nodeVo = (TsiNodeVo)cpuPacket.getObj();//TsiNodeManager.getInstance().get(packet.getNodeId());

        long curr = System.nanoTime();
        if (TimeUnit.MILLISECONDS.convert(curr - packet.getRcv(), TimeUnit.NANOSECONDS) > 3000) {
            log.error("Packet skip::: {}, {} ms.", packet.getNodeId(), TimeUnit.MILLISECONDS.convert(curr - packet.getRcv(), TimeUnit.NANOSECONDS));
            return;
        }
        packet.setPop(curr);

        if (packet.getOpCode() == (byte) eOpCode.TSI_CPU_DISCONNECTED.getValue()) {
            TsiCpuDisconnected disconnected = (TsiCpuDisconnected)packet;
            disconnected.parsing(nodeVo);
            if (nodeVo != null) {
                if (nodeVo.isSendCvim() && disconnected.getCvimData() != null) {
                    this.kafkaProducer.sendCvim(disconnected.getNodeId(), disconnected.getCvimData());
                }

                if (disconnected.getAddNodes() != null) {
                    // 연등지 인 경우
                    for (int ii = 0; ii < disconnected.getAddNodes().size(); ii++) {
                        this.kafkaProducer.sendCvim(disconnected.getAddNodes().get(ii).getNodeId(), disconnected.getAddNodes().get(ii).getCvimData());
                    }
                }
                this.loggingService.add(packet, this.idx);//(int)Thread.currentThread().getId());
            }
            return;
        }

        // 메모리에서 객체를 찾은 후 패킷 파싱
        if (!cpuPacket.parsing(nodeVo)) {
            MDC.put("id", nodeVo.getKey());
            log.error("Packet parsing failed: {}", packet.getNodeId());
            MDC.clear();
        }
        packet.setPar(System.nanoTime());

        // 카프카 전송
        if (nodeVo != null) {
            if (nodeVo.isSendNode() && packet.getNodeData() != null) {
                this.kafkaProducer.sendNode(Long.toString(packet.getNodeId()), packet.getNodeData());
            }
            if (nodeVo.isSendTest()) {
                this.kafkaProducer.sendTest(packet.getNodeId(), packet.getTestData());
            }
            if (nodeVo.isSendCvim() && packet.getCvimData() != null) {
                this.kafkaProducer.sendCvim(packet.getNodeId(), packet.getCvimData());
            }

            if (cpuPacket.getAddNodes() != null) {
                // 연등지 인 경우
                for (int ii = 0; ii < cpuPacket.getAddNodes().size(); ii++) {
                    if (nodeVo.isSendNode()) {
                        this.kafkaProducer.sendNode(Long.toString(cpuPacket.getAddNodes().get(ii).getNodeId()), cpuPacket.getAddNodes().get(ii).getNodeData());
                    }
                    if (nodeVo.isSendCvim()) {
                        this.kafkaProducer.sendCvim(cpuPacket.getAddNodes().get(ii).getNodeId(), cpuPacket.getAddNodes().get(ii).getCvimData());
                    }
                }
            }
        }
        else {
            // 여기 들어오면 안됌
            MDC.put("id", Long.toString(packet.getNodeId()));
            log.warn("Not found node object: {}", packet.getNodeId());
            MDC.clear();
            // kafka send to topic-for-ssd-test only
            this.kafkaProducer.sendTest(packet.getNodeId(), packet.getTestData());
        }

        packet.setEnd(System.nanoTime());

        packet.setAvg(calcProcessTime(packet.getRcv()));

        // 로그큐로 전송한다.
        this.loggingService.add(packet, this.idx);//(int)Thread.currentThread().getId());

        // 연등지 인 경우
        if (cpuPacket.getAddNodes() != null) {
            for (int ii = 0; ii < cpuPacket.getAddNodes().size(); ii++) {
                cpuPacket.getAddNodes().get(ii).setAdd(packet.getAdd());
                cpuPacket.getAddNodes().get(ii).setPop(packet.getPop());
                cpuPacket.getAddNodes().get(ii).setPar(packet.getPar());
                cpuPacket.getAddNodes().get(ii).setEnd(packet.getEnd());
                cpuPacket.getAddNodes().get(ii).setAvg(packet.getAvg());
                this.loggingService.add(cpuPacket.getAddNodes().get(ii), this.idx);
            }
        }

    }

    public void report() {
        long avgTime = 0;
        log.info("Packet: Remain Q: {}, Average: {}, {}", this.DATA_QUEUE.size(), TimeUtils.elapsedTimeStr(avgTime), Thread.currentThread().getName());
    }
}
