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

import com.tsi.comm.server.config.TraceConfig;
import com.tsi.comm.server.config.TsiCvimServerConfig;
import com.tsi.comm.server.kafka.KafkaProducerService;
import com.tsi.comm.server.process.AbstractTsiCvimWorker;
import com.tsi.comm.server.process.logging.TsiCvimLoggingProcess;
import com.tsi.comm.server.protocol.*;
import com.tsi.comm.server.protocol.enums.eOpCode;
import com.tsi.comm.server.vo.TsiNodeVo;
import com.tsi.common.spring.SpringUtils;
import com.tsi.common.utils.TimeUtils;
import io.netty.buffer.ByteBufUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;

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

@Slf4j
@Getter
public class TsiCvimPacketWorker extends AbstractTsiCvimWorker implements Runnable {

    public static final AbstractTsiPacket SHUTDOWN_PACKET = new TsiCpuPacket(-1L, 0, 0, 0, 0);

    private final LinkedBlockingQueue<AbstractTsiPacket> DATA_QUEUE;

    private final KafkaProducerService kafkaProducer;
    private final TsiCvimLoggingProcess loggingProcess;
    private final TraceConfig traceConfig;
    private final TsiCvimServerConfig serverConfig;

    public TsiCvimPacketWorker(int idx, int qSize, KafkaProducerService kafkaProducer) {
        this.idx = idx;
        this.qSize = qSize;
        this.kafkaProducer = kafkaProducer;
        this.DATA_QUEUE = new LinkedBlockingQueue<>(qSize);
        this.loggingProcess = SpringUtils.getBean(TsiCvimLoggingProcess.class);
        this.traceConfig = SpringUtils.getBean(TraceConfig.class);
        this.serverConfig = SpringUtils.getBean(TsiCvimServerConfig.class);
    }

    @Override
    public void run() {
        log.info("PacketWorker({}): {} Start. QSIZE: {}", this.idx, Thread.currentThread().getName(), this.qSize);
        try {
            while (!Thread.currentThread().isInterrupted()) {
                AbstractTsiPacket packet = null;
                try {
                    packet = this.DATA_QUEUE.take();
                    if (packet == SHUTDOWN_PACKET) {
                        log.info("PacketWorker({}): Shutdown signal received. Exiting...", this.idx);
                        break;
                    }
                    process(packet);
                } catch (InterruptedException ie) {
                    log.warn("PacketWorker({}): {} Interrupted. Exiting...", this.idx, Thread.currentThread().getName());
                    Thread.currentThread().interrupt(); // 상태 복구
                    break;
                } catch (Exception e) {
                    if (packet != null) {
                        TsiCpuPacket cpuPacket = (TsiCpuPacket) packet;
                        TsiNodeVo nodeVo = (TsiNodeVo)cpuPacket.getObj();
                        if (nodeVo != null) {
                            log.warn("PacketWorker({}): {} Node: {}, Network Conn: {}", this.idx, Thread.currentThread().getName(), nodeVo.getKey(), nodeVo.isConnect());
                        }
                    }
                    else {
                        log.error("PacketWorker({}): {} Exception: {}", this.idx, Thread.currentThread().getName(), e.getMessage());
                    }
                } finally {
                    if (packet != null && packet != SHUTDOWN_PACKET) {
                        TsiCpuPacket cpuPacket = (TsiCpuPacket)packet;
                        TsiNodeVo nodeVo = (TsiNodeVo)cpuPacket.getObj();
                        if (nodeVo != null) {
                            // 패킷 처리 완료
                            nodeVo.packetProcessingFinished(cpuPacket);
                        }
                    }
                }
            }
        } finally {
            log.info("PacketWorker({}): {} Stopped.", this.idx, Thread.currentThread().getName());
        }
    }

    /*
     *  작업큐에 데이터 추가
     */
    public boolean add(Object object) {
        boolean offer = false;
        AbstractTsiPacket packet = (AbstractTsiPacket)object;
        try {
            offer = this.DATA_QUEUE.offer(packet);  //offer => full -> return, add => full -> wait
            if (!offer) {
                MDC.put("id", Long.toString(packet.getNodeId()));
                log.error("PacketWorker({}): Queue.offer(Full): {}, {}/{}/{}, {}, {}",
                        this.idx, packet.getNodeId(),
                        this.DATA_QUEUE.size(), this.qSize, this.DATA_QUEUE.remainingCapacity(),
                        TimeUtils.elapsedTime(packet.getRcv()), Thread.currentThread().getName());
                MDC.clear();
            }
        } catch (Exception e) {
            MDC.put("id", Long.toString(packet.getNodeId()));
            log.error("PacketWorker({}): Queue.offer: Exception: {}, {}, {}", this.idx, 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 nodeId = packet.getNodeId();
        if (nodeVo == null) {
            // 노드 정보가 없는 경우
            log.error("Packet process NodeVo is null: {}, {}", nodeId, Thread.currentThread().getName());
            return;
        }

        if (!nodeVo.isRegistered()) {
            // 등록되지 않은 노드인 경우 다른 처리를 하지 않는다.
            return;
        }

        long curr = System.nanoTime();
        packet.setPop(curr);

        MDC.put("id", nodeVo.getKey());
        try {
            if (isPacketDelayed(cpuPacket, curr)) {
                return;
            }

            if (isDisconnectedPacket(cpuPacket)) {
                handleDisconnectedPacket(cpuPacket, nodeVo);
            }
            else {
                handleDataPacket(cpuPacket, nodeVo);
            }
        } finally {
            MDC.clear();
        }
    }

    /**
     * 일반 데이터 패킷에 대한 처리
     */
    private void handleDataPacket(TsiCpuPacket cpuPacket, TsiNodeVo nodeVo) {
        // 20250425: packet parsing 의 로그를 노드별 로그파일에 저장되도록 MDC 위치 변경
        // 20250425: parsing 함수에 packet-check 여부를 같이 넘겨줘서 CRC 체크여부 확인

        // 1. 파싱 및 유효성 검사
        if (!parseAndValidate(cpuPacket, nodeVo)) {
            return; // 파싱 실패 시 처리 중단
        }
        cpuPacket.setPar(System.nanoTime());

        // 2. Kafka로 데이터 전송
        sendToKafka(cpuPacket, nodeVo);
        cpuPacket.setEnd(System.nanoTime());

        // 3. 통계 및 로깅
        cpuPacket.setAvg(calcProcessTime(cpuPacket.getRcv()));

        logPacketIfNeeded(cpuPacket, nodeVo);
    }

    private void sendToKafka(TsiCpuPacket cpuPacket, TsiNodeVo nodeVo) {
        if (nodeVo.isSendNode() && cpuPacket.getNodeData() != null) {
            this.kafkaProducer.sendNode(Long.toString(nodeVo.getNodeId()), cpuPacket.getNodeData());
        }
        if (nodeVo.isSendTest() && cpuPacket.getTestData() != null) {
            this.kafkaProducer.sendTest(nodeVo.getNodeId(), cpuPacket.getTestData());
        }
        if (nodeVo.isSendCvim() && cpuPacket.getCvimData() != null) {
            this.kafkaProducer.sendCvim(nodeVo.getNodeId(), cpuPacket.getCvimData());
        }

        if (cpuPacket.getAddNodes() != null) {
            for (TsiCpuAddPacket addNodePacket : cpuPacket.getAddNodes()) {
                // 연등지 노드 패킷 카프카 전송
                if (nodeVo.isSendNode()) {
                    this.kafkaProducer.sendNode(Long.toString(addNodePacket.getNodeId()), addNodePacket.getNodeData());
                }
                if (nodeVo.isSendCvim()) {
                    this.kafkaProducer.sendCvim(addNodePacket.getNodeId(), addNodePacket.getCvimData());
                }
            }
        }
    }

    private boolean parseAndValidate(TsiCpuPacket cpuPacket, TsiNodeVo nodeVo) {
        try {
            int result = cpuPacket.parsing(nodeVo, this.serverConfig.isCheckPacket());
            if (result == 0) {
                return true; // 파싱 성공
            }

            if (!nodeVo.isConnect()) {
                log.error("parsing: errno({}), NodeId: {}, Disconnected.", result, nodeVo.getNodeId());
                return false;   // 네트워크 연결이 끊어진 경우
            }

            if (-4 == result) {
                return false; // 패킷 버퍼가 null 이거나 너무 짧은 경우
            }

            if (cpuPacket.getBuf() != null) {
                byte version = 0x00;
                byte[] buf = cpuPacket.getBuf();
                if (buf.length > TsiCvibProtocolSpec.INDEX_VERSION) {
                    version = buf[TsiCvibProtocolSpec.INDEX_VERSION];
                }
                log.error("parsing: errno({}), NodeId: {}, version: {}", nodeVo.getNodeId(), result, version);
                log.error("parsing: {}", ByteBufUtil.hexDump(buf));
            }
            return false;
        }
        catch (Exception e) {
            log.warn("Node: {}, CPU Packet parsing error: {}, connect: {}, {}", nodeVo.getNodeId(), Thread.currentThread().getName(), nodeVo.isConnect(), e.getMessage());
            return false;
        }
    }

    /**
     * 연결 종료 패킷에 대한 처리
     */
    private void handleDisconnectedPacket(TsiCpuPacket packet, TsiNodeVo nodeVo) {
        TsiCpuDisconnected disconnected = (TsiCpuDisconnected) packet;
        try {
            if (!disconnected.parsing(nodeVo)) {
                // nodeVo == null
                return;
            }
            packet.setPar(System.nanoTime());

            if (nodeVo.isSendCvim() && disconnected.getCvimData() != null) {
                this.kafkaProducer.sendCvim(disconnected.getNodeId(), disconnected.getCvimData());
            }

            if (nodeVo.isSendCvim() && disconnected.getAddNodes() != null) {
                // 연등지 인 경우
                for (TsiCpuPacket addNodePacket : disconnected.getAddNodes()) {
                    this.kafkaProducer.sendCvim(addNodePacket.getNodeId(), addNodePacket.getCvimData());
                }
            }
            // 2. Kafka로 데이터 전송
            packet.setEnd(System.nanoTime());

            // 3. 통계 및 로깅
            packet.setAvg(calcProcessTime(packet.getRcv()));

            logPacketIfNeeded(packet, nodeVo);
        } catch (Exception e) {
            if (nodeVo.isConnect()) {
                log.warn("Node: {}, Disconnect parsing error: {}, connect: {}, {}", nodeVo.getNodeId(), Thread.currentThread().getName(), nodeVo.isConnect(), e.getMessage());
            }
        }
    }

    /**
     * 연결 종료 패킷 여부 확인
     */
    private boolean isDisconnectedPacket(TsiCpuPacket packet) {
        return packet.getOpCode() == (byte) eOpCode.TSI_CPU_DISCONNECTED.getValue();
    }

    /**
     * 수신한 패킷의 시간초과 여부 체크
     * 큐에 수신한 시각과 큐에서 읽어 낸 시간의 차이를 계산
     */
    private boolean isPacketDelayed(TsiCpuPacket packet, long curr) {
        final long delayMillis = TimeUnit.NANOSECONDS.toMillis(curr - packet.getRcv());
        if (delayMillis > 3000) {
            log.warn("Packet skip::: {}, {} ms.", packet.getNodeId(), delayMillis);
            return true;
        }
        return false;
    }

    /**
     * 로깅 여부에 따라 로길 프로세스로 데이터 전송
     */
    private void logPacketIfNeeded(AbstractTsiPacket packet, TsiNodeVo nodeVo) {
        if (this.traceConfig.isNodeLogging() || nodeVo.isDump()) {

            this.loggingProcess.add(packet, nodeVo.getLogQIdx());

            if (packet instanceof TsiCpuPacket) {
                TsiCpuPacket cpuPacket = (TsiCpuPacket) packet;
                if (cpuPacket.getAddNodes() != null) {
                    // 연등지 인 경우 연등지 패킷 로깅
                    for (TsiCpuAddPacket addNodePacket : cpuPacket.getAddNodes()) {
                        addNodePacket.setAdd(packet.getAdd());
                        addNodePacket.setPop(packet.getPop());
                        addNodePacket.setPar(packet.getPar());
                        addNodePacket.setEnd(packet.getEnd());
                        addNodePacket.setAvg(packet.getAvg());

                        this.loggingProcess.add(addNodePacket, nodeVo.getLogQIdx());
                    }
                }
            }
        }
    }

    public void report() {
        log.info("PacketWorker({}), Queue Total/Size/Remain: {}/{}/{}, Average: {}, {}",
                this.idx,
                this.qSize, this.DATA_QUEUE.size(), this.DATA_QUEUE.remainingCapacity(),
                TimeUtils.elapsedTimeStr(this.avgTime),
                Thread.currentThread().getName());
    }

}
