package com.tsi.comm.server.tcp.handler;

import com.tsi.comm.server.config.TsiCvimServerConfig;
import com.tsi.comm.server.process.dbms.TsiCvimDbmsProcess;
import com.tsi.comm.server.process.packet.TsiChannelSession;
import com.tsi.comm.server.process.packet.TsiCvimPacketProcess;
import com.tsi.comm.server.protocol.TsiCpuPacket;
import com.tsi.comm.server.repository.TsiAlarmManager;
import com.tsi.comm.server.repository.TsiNodeManager;
import com.tsi.comm.server.repository.TsiSessionManager;
import com.tsi.comm.server.repository.TsiTpmsManager;
import com.tsi.comm.server.service.TsiQueueDistributorService;
import com.tsi.comm.server.vo.TsiAlarmConfigVo;
import com.tsi.comm.server.vo.TsiNodeVo;
import com.tsi.comm.server.vo.mariadb.AbstractDbmsVo;
import com.tsi.comm.server.vo.mariadb.AlarmOccrVo;
import com.tsi.comm.server.vo.mariadb.NodeIpAddrVo;
import com.tsi.comm.server.vo.mariadb.NodeStatusVo;
import com.tsi.comm.server.xnet.NettyUtils;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
@ChannelHandler.Sharable
public class CvimServerPacketProcessHandler extends SimpleChannelInboundHandler<TsiCpuPacket> {

    private final TsiCvimServerConfig config;
    private final TsiNodeManager nodeManager;
    private final TsiAlarmManager alarmManager;
    private final TsiTpmsManager tpmsManager;
    private final TsiSessionManager sessionManager;
    private final TsiCvimPacketProcess packetProcess;
    private final TsiCvimDbmsProcess dbmsProcess;
    private final TsiQueueDistributorService queueDistributorService;


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TsiCpuPacket packet) throws Exception {

        // 채널이 '미인증' 상태로 표시되어 있는지 먼저 확인한다.
//        if (ctx.channel().hasAttr(TsiSessionManager.UNAUTHENTICATED_KEY)) {
//            // '미인증' 상태라명, 더 이상 아무 처리도 하지 않고 즉시 리턴.
//            return;
//        }

        final String remoteIpAddress = NettyUtils.getRemoteIpAddress(ctx.channel());
        final long nodeId = packet.getNodeId();
        TsiNodeVo nodeVo = ctx.channel().attr(TsiSessionManager.TSI_NODE_ATTRIBUTE_KEY).get();
        if (nodeVo == null) {
            // 통신 연결 후에 첫번째 패킷을 수신한 경우의 처리임.
//            if (nodeId <= 0) {
//                // '미인증' 상태를 채널 속성에 저장.
//                // 잦은 연결/종료를 해결하기 위해 기존 종료시켰던 로직을 연결을 유지하도록 수정함.
//                ctx.channel().attr(TsiSessionManager.UNAUTHENTICATED_KEY).set(true);
//
//                byte[] buf = packet.getBuf();
//                this.alarmManager.loggingUnknownNodePacket(nodeId, ctx.channel(), buf);
//                // 연결을 끊지 않고, 처리를 여기서 종료.
//                return;
//            }

            // 정상적인 첫번째 연결: 채널맵에 등록
            nodeVo = attachNodeVo(nodeId, remoteIpAddress, ctx.channel());
            if (nodeVo == null) {
                // node id 가 0 이하로 설정되어 있거나, 노드 정보가 등록되어 있지 않은 경우
                if (nodeId <= 0) {
                    byte[] buf = packet.getBuf();
                    this.alarmManager.loggingUnknownNodePacket(nodeId, ctx.channel(), buf);
                }
                ctx.close();
                return;
            }

            // first connection, save node information to channel attribute map
            // 채널 맵에 채널에 대한 노드정보를 저장한다.
            // 채널 목록에 접속 정보 등록(중복 로그인 체크)
            Channel oldChannel = nodeVo.connectChannel(ctx.channel(), this.alarmManager, remoteIpAddress);

            this.sessionManager.addChannel(ctx.channel(), nodeVo);
            TsiChannelSession.objectRegistered(nodeVo, remoteIpAddress);

            this.queueDistributorService.setLoadedQueue(nodeVo);    // 작업큐를 할당한다.

            ctx.channel().attr(TsiSessionManager.PKT_Q_IDX_KEY).set(nodeVo.getPktQIdx());       // packet q index
            ctx.channel().attr(TsiSessionManager.DBMS_Q_IDX_KEY).set(nodeVo.getDbmsQIdx());     // dbms q index
            ctx.channel().attr(TsiSessionManager.LOG_Q_IDX_KEY).set(nodeVo.getLogQIdx());       // log q index

            ctx.channel().attr(TsiSessionManager.TSI_NODE_ATTRIBUTE_KEY).set(nodeVo);           // node object

            // 최초 접속시 접속안 IP와 DB 설정 IP 비교
            if (!remoteIpAddress.equals(nodeVo.getIpAddr())) {
                // IP 주소가 변경된 경우
                final String oldIpAddr = nodeVo.getIpAddr();
                log.warn("Node IpAddr Changed: {} -> {}", oldIpAddr, remoteIpAddress);
                nodeVo.setIpAddr(remoteIpAddress);

                NodeIpAddrVo nodeIpAddrVo = new NodeIpAddrVo(AbstractDbmsVo.DBMS_NODE_IP_UPDATE);
                nodeIpAddrVo.setNodeId(nodeVo.getNodeId());
                nodeIpAddrVo.setIpAddr(remoteIpAddress);

                this.nodeManager.putIpAddr(remoteIpAddress, nodeVo); // IP 주소 갱신

                this.dbmsProcess.add(nodeIpAddrVo, nodeVo.getDbmsQIdx());
            }

            if (nodeVo.isRegistered()) {
                // DB에 등록된 교차로에 대해서 상태정보 업데이트
                NodeStatusVo status = new NodeStatusVo(AbstractDbmsVo.DBMS_NODE_STATUS);
                status.setServerId(this.config.getServerId());
                status.setNodeId(nodeId);
                status.setStatus(1);
                status.setIpAddr(NettyUtils.getRemoteIpAddress(ctx.channel()));

                // 신호제어기 통신상태 업데이트
                this.dbmsProcess.add(status, nodeVo.getDbmsQIdx());
            }
        }

        final long DUPLICATE_PACKET_INTERVAL_MS = 600;
        if (!nodeVo.isProcessable(DUPLICATE_PACKET_INTERVAL_MS)) {
            return; // 패킷을 버리고 처리를 종료
        }

        nodeVo.packetProcessingStarted();   // 패킷 처리시작

        packet.setObj(nodeVo); // 핸들러에서 찾아낸 최종 nodeVo를 패킷에 설정

        nodeVo.setLastCommTm(System.currentTimeMillis());   // 통신 수신시각 저장

        // 작업큐로 데이터 전송
        this.packetProcess.add(packet, nodeVo.getPktQIdx());    // 패킷인덱스큐로 데이터 전송

        // 작업큐 데이터 전송 시각 설정
        packet.setAdd(System.nanoTime());

        // 패킷 통계정보 생성
        this.tpmsManager.readPacket(packet);

        if (nodeVo.isDump()) {
            // 패킷 덤프
            TsiChannelSession.recvPacketDump(nodeVo, packet);
        }
    }

    private TsiNodeVo attachNodeVo(long nodeId, String remoteIpAddress, Channel channel) {

        // 노드맵에서 노드 정보를 찾는다.
        return this.nodeManager.getOrCompute(nodeId, id -> {
            // 맵에 nodeId가 존재하지 않을 때만 실행됨, 존재하는 경우는 해당 객체가 리턴됨
            // 메모리맵에 노드정보가 존재하지 않은 경우 로깅
            this.alarmManager.loggingUnknownNode(id, channel);

            if (id <= 0) {
                // 노드 ID가 0인것들은 연결을 종료한다.(맵에 저장되지 않음)
                return null;
            }

            // 등록되어 있지 않은 노드 ID로 접속한 경우(노드 ID가 0 이하인 경우 제외)
            // 신규로 해당 노드정보 메모리를 생성해서 메모리 맵에 추가한다.
            // 신규 TsiNodeVo 객체 생성
            log.info("Creating new TsiNodeVo for unregistered node ID: {}", id);
            TsiNodeVo newNodeVo = new TsiNodeVo(id, remoteIpAddress, false, true, true);
            newNodeVo.setCheckInstalled(true);
            newNodeVo.setRegistered(false);

            // this.nodeManager.put(id, newNodeVo); // computeIfAbsent 에서 알아서 put 해준다.
            this.nodeManager.putIpAddr(remoteIpAddress, newNodeVo); // IP 주소 갱신

            // oops 알람 설정 되어 있으면 알람테이블에 저장하자.
            if (this.alarmManager.checkAlarm(TsiAlarmConfigVo.COMM_01)) {
                AlarmOccrVo alarm = new AlarmOccrVo(AbstractDbmsVo.DBMS_ALARM_OCCR_HS);
                alarm.setAlarmCode(TsiAlarmConfigVo.COMM_01);
                alarm.setAlarmTarget(Long.toString(id));
                alarm.setAlarmValue(remoteIpAddress);

                this.dbmsProcess.add(alarm, 0); // 아직 큐인덱스를 설정하기 이전임
            }
            return newNodeVo;
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        TsiNodeVo nodeVo = ctx.channel().attr(TsiSessionManager.TSI_NODE_ATTRIBUTE_KEY).get();

        if (cause instanceof CorruptedFrameException) {
            // 디코딩 단계에서 발생한, 예측된 프레임 오류
            log.warn("Exception CorruptedFrameException {}: {}", NettyUtils.getAddress(ctx.channel()), cause.getMessage());
        } else if (nodeVo != null) {
            // 노드가 식별된 이후에 발생한 예외
            log.error("Exception processing packet, Node ID {}:", nodeVo.getNodeId(), cause);
        } else {
            // 노드가 식별되기 전에 발생한 예외 (Decoder 포함)
            log.error("Exception in pipeline for channel {}:", NettyUtils.getAddress(ctx.channel()), cause);
        }

        ctx.close();
    }
}
