package com.its.vds.dto;

import com.its.app.utils.SysUtils;
import com.its.vds.domain.NET;
import com.its.vds.domain.VdsDebug;
import com.its.vds.domain.VdsFrameSequence;
import com.its.vds.domain.task.VdsCommandTimeoutTask;
import com.its.vds.entity.TbVdsCtlr;
import com.its.vds.entity.TbVdsCtlrStts;
import com.its.vds.entity.TbVdsCtrlHs;
import com.its.vds.entity.TbVdsDtct;
import com.its.vds.xnettcp.vds.handler.VdsServerIdleStateHandler;
import com.its.vds.xnettcp.vds.process.TcpServerSendData;
import com.its.vds.xnettcp.vds.process.TcpServerSendDataProcess;
import com.its.vds.xnettcp.vds.protocol.VdsReqFramePacket;
import com.its.vds.xnettcp.vds.protocol.enums.eVdsOpCode;
import com.its.vds.xnettcp.vds.protocol.impl.VdsReqDataLength;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import lombok.*;
import lombok.extern.slf4j.Slf4j;

import java.io.Serializable;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Map;
import java.util.Timer;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Getter
@Setter
@ToString
@Builder
@NoArgsConstructor//(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class TbVdsCtlrDto implements Serializable {
    private static final long serialVersionUID = 1L;

    private int  index;

    private Long vdsCtlrNmbr;
    private String vdsCtlrId;
    private String vdsNm;
    private String vdsCtlrIp;
    private Integer vdsCtlrPort;
    private Integer vdsCtlrLocalNo;
    private String vdsTypeCd;
    private String istlLctnNm;
    private Integer groupNo;
    private Integer fanMode;
    private Integer fanRunTmpr;
    private Integer hetrMode;
    private Integer hetrRunTmpr;
    private Integer detectLanes;
    private Integer trafClctCycl;
    private Integer sttsClctCycl;
    private String istlLctnAddr;
    private String validYn;
    private String delYn;

    private Map<String, TbVdsDtct> 	vdsDtctMap;
    private TbVdsCtlrStts     stts;
    private int 			  netState;
    private boolean           isDupCon;
    private boolean           isDupLogin;
    private String 		      dstIpAddr;
    private Channel           channel = null;
    private Channel           dupChannel = null;
    private InetSocketAddress cliReq;
    private long              syncTime;
    private VdsFrameSequence  seq;

    private ConcurrentHashMap<Short, Timer> registeredCommandTimer = null;
    private ConcurrentHashMap<Short, VdsCommandTimeoutTask> registeredCommand = null;
    private ConcurrentHashMap<Long, TbVdsCtrlHs> userCommands = null;

    private int connectCount;
    private String connectTm;
    private String disConnectTm;
    private long lastRecvTime;

    private VdsDebug debug;

    private TcpServerSendDataProcess dataProcess;   // 제어기로 데이터를 전송할 전송 전송 큐를 처리할 스레드

    public void init() {
        this.debug = new VdsDebug();

        this.stts = new TbVdsCtlrStts(this.vdsCtlrNmbr);
        this.seq  = new VdsFrameSequence();

        this.registeredCommandTimer = new ConcurrentHashMap<>();
        this.registeredCommand = new ConcurrentHashMap<>();

        this.userCommands = new ConcurrentHashMap<>();

        this.connectCount = 0;
        this.connectTm = "-";
        this.disConnectTm = "-";
        this.lastRecvTime = 0;

        initNet();

        this.dataProcess = new TcpServerSendDataProcess(this);
        this.dataProcess.run();
    }

    public String getLogKey() {
        return this.vdsCtlrId;
    }

    /**
     * 제어기 전송 패킷의 패킷 번호를 구한다.
     * @return
     */
    public short getSeqNext() {
        return this.seq.nextValue();
    }

    /**
     * 제어기 전송 스레드 타스크 큐에 명령을 전송
     * @param data
     * @return
     */
    public synchronized boolean addRequestData(TcpServerSendData data) {
        return this.dataProcess.add(data);
    }

    /**
     * 제어기 네트워크 최초 접속시 발생 이벤트
     * @param ctx
     */
    public synchronized void connected(ChannelHandlerContext ctx) {
        this.netState = NET.LOGIN_REQ;
        this.connectTm = SysUtils.getSysTimeStr();
        this.channel = ctx.channel();
        this.lastRecvTime = System.currentTimeMillis();
        this.connectCount++;
        this.stts.initNormal();
        clearRegisteredCommandTimer();
    }

    /**
     * 제어기 네트워크 연결 종료 이벤트
     */
    public synchronized void disconnected() {
        clearRegisteredCommandTimer();
        this.netState = NET.CLOSED;
        this.channel = null;
        this.disConnectTm = SysUtils.getSysTimeStr();
        this.stts.initError();
    }

    public String getLastRecvTimeFmt() {
        SimpleDateFormat sdfDate = new SimpleDateFormat("MM-dd HH:mm:ss");
        return sdfDate.format(this.lastRecvTime);
    }

    public void resetConnectCount() {
        if (this.netState == NET.CLOSED)
            this.connectCount = 0;
        else
            this.connectCount = 1;
    }

    void initNet() {
        this.netState   = NET.CLOSED;
        this.isDupCon   = false;
        this.isDupLogin = false;
        this.dstIpAddr  = "";
        this.channel    = null;
        this.dupChannel = null;
        this.cliReq     = null;
        this.syncTime   = 0;
    }

    /**
     * 제어기 명령 타임아웃 타스크 클리어
     */
    public synchronized void clearRegisteredCommandTimer() {
        this.registeredCommand.clear();
        for (Map.Entry<Short, Timer> e : this.registeredCommandTimer.entrySet()) {
            Timer task = e.getValue();
            task.cancel();
        }
        this.registeredCommandTimer.clear();
    }

    /**
     * 제어기 명령 타임아웃 타스크 추가
     * @param cmdTimeoutTask
     * @return
     */
    public synchronized void addRegisteredCommandsTimer(VdsCommandTimeoutTask cmdTimeoutTask) {
        long timeoutSec = 1000L * 5;
        Timer timer = new Timer();
        timer.schedule(cmdTimeoutTask, timeoutSec);
        this.registeredCommandTimer.put(cmdTimeoutTask.getPacketNmbr(), timer);
        this.registeredCommand.put(cmdTimeoutTask.getPacketNmbr(), cmdTimeoutTask);
//        log.info("addRegisteredCommandsTimer: VDS {}, Task {} EA, OpCode {}.",
//                cmdTimeoutTask.getCtlr().getVdsCtlrNmbr(), this.registeredCommandTimer.size(), cmdTimeoutTask.getPacket().getOpCode());
    }

    /**
     * 제어기 명령 타임아웃 타스크 제거
     * @param opCode
     */
    public synchronized void removeRegisteredCommandsTimer(eVdsOpCode opCode) {
        short packetNmbr = (short)opCode.getValue();
        Timer timer = this.registeredCommandTimer.get(packetNmbr);
        if (timer != null) {
            timer.cancel();
            this.registeredCommandTimer.remove(packetNmbr);
        }
        VdsCommandTimeoutTask cmdTimeoutTask = this.registeredCommand.get(packetNmbr);
        if (cmdTimeoutTask != null) {
            this.registeredCommand.remove(packetNmbr);
//            log.info("removeRegisteredCommandsTimer: VDS {}, Task {} EA, OpCode {}.",
//                    cmdTimeoutTask.getCtlr().getVdsCtlrNmbr(), this.registeredCommandTimer.size(), cmdTimeoutTask.getPacket().getOpCode());
        }
    }

    /**
     * Channel Send Data
     * @param packet
     * @param retryCnt
     * @return
     */
    public synchronized boolean sendData(VdsReqFramePacket packet, int retryCnt) {
        boolean result = false;

        ByteBuffer sendBuffer = packet.getByteBuffer();
        String packetDesc = packet.getOpCodeDesc();

        log.info("~REQUEST-{}. VDS {}, {} Bytes.", packetDesc, this.vdsCtlrNmbr, sendBuffer.capacity());
        if (this.channel != null) {
            ChannelFuture f = this.channel.writeAndFlush(sendBuffer);
            f.awaitUninterruptibly();
            if (f.isDone() || f.isSuccess()) {
                result = true;
            } else {
                log.error("~REQUEST-{}. VDS {}, sendData Failed. {} Bytes.", packetDesc, this.vdsCtlrNmbr, sendBuffer.array().length);
            }
        } else {
            log.error("~REQUEST-{}. VDS {}, sendData Failed. Not Connected. {} Bytes.", packetDesc, this.vdsCtlrNmbr, sendBuffer.array().length);
        }
        if (result) {
            VdsCommandTimeoutTask cmdTimeoutTask = new VdsCommandTimeoutTask(this, packet, retryCnt);
            addRegisteredCommandsTimer(cmdTimeoutTask);
        }
        return result;
    }

    public boolean channelClose() {
        if (getChannel() == null || getNetState() == NET.CLOSED) {
            log.error("Close Request: channel not connected: [{}]", this);
            return false;
        }
        VdsServerIdleStateHandler.disconnectChannel(this, getChannel());
        return true;
    }

    public boolean reset() {
        if (getChannel() == null || getNetState() == NET.CLOSED) {
            log.error("Reset Request: channel not connected: [{}]", this);
            return false;
        }
        VdsReqDataLength resetControl = new VdsReqDataLength(this);
//        resetControl.controlReset();
        return addRequestData(new TcpServerSendData(eVdsOpCode.OP_VDS_DATA_LENGTH, resetControl));
    }

    public synchronized boolean removeUserCommands(Long packetNmbr) {

        TbVdsCtrlHs command = this.userCommands.get(packetNmbr);
        if (command != null) {
            this.userCommands.remove(packetNmbr);
            return true;
        }
        return false;
    }

    public synchronized TbVdsCtrlHs getUserCommands(Long packetNmbr) {

        TbVdsCtrlHs command = this.userCommands.get(packetNmbr);
        return command;
    }

    public TbVdsCtlr toEntity() {
        return TbVdsCtlr.builder()
                .vdsCtlrNmbr(this.vdsCtlrNmbr)
                .vdsCtlrId(this.vdsCtlrId)
                .vdsNm(this.vdsNm)
                .vdsCtlrIp(this.vdsCtlrIp)
                .vdsCtlrPort(this.vdsCtlrPort)
                .vdsCtlrLocalNo(this.vdsCtlrLocalNo)
                .vdsTypeCd(this.vdsTypeCd)
                .istlLctnNm(this.istlLctnNm)
                .groupNo(this.groupNo)
                .fanMode(this.fanMode)
                .fanRunTmpr(this.fanRunTmpr)
                .hetrMode(this.hetrMode)
                .hetrRunTmpr(this.hetrRunTmpr)
                .detectLanes(this.detectLanes)
                .trafClctCycl(this.trafClctCycl)
                .sttsClctCycl(this.sttsClctCycl)
                .istlLctnAddr(this.istlLctnAddr)
                .validYn(this.validYn)
                .delYn(this.delYn)
                .build();
    }

}

