package com.njcn.gather.detection.util.socket.websocket;

import com.njcn.gather.detection.util.socket.SocketManager;
import com.njcn.gather.detection.util.socket.communication.model.ConnectionType;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;

import static com.njcn.gather.detection.util.socket.websocket.WebSocketConstants.HEARTBEAT_PING;
import static com.njcn.gather.detection.util.socket.websocket.WebSocketConstants.HEARTBEAT_PONG;
import static com.njcn.gather.detection.util.socket.websocket.WebSocketConstants.MAX_HEARTBEAT_MISS_COUNT;

/**
 * Generic WebSocket handler retained by the communication foundation.
 * Stage 4-A keeps only handshake, heartbeat, session registry and transport
 * cleanup. Detection-specific quit flows are removed from this class.
 *
 * @author wr
 * @author hongawen
 * @date 2026/04/07
 */
@Slf4j
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    private static final String HEARTBEAT_RESPONSE_TEXT = HEARTBEAT_PONG;

    private int times;

    private String userId;

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("WebSocket channel active: channelId={}", ctx.channel().id());
        super.channelActive(ctx);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
        String messageText = msg.text();
        if (HEARTBEAT_PING.equals(messageText)) {
            handleHeartbeat(ctx);
            return;
        }
        log.debug("Receive WebSocket business message: userId={}, channelId={}, message={}",
                userId, ctx.channel().id(), messageText);
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        log.info("WebSocket handler added: channelId={}", ctx.channel().id());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        log.info("WebSocket handler removed: channelId={}, userId={}", ctx.channel().id(), userId);
        if (userId != null) {
            WebServiceManager.removeByUserId(userId);
        } else {
            WebServiceManager.removeChannel(ctx.channel().id().toString());
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("WebSocket channel inactive: channelId={}, userId={}", ctx.channel().id(), userId);
        cleanupSocketResources(userId);
        super.channelInactive(ctx);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
            WebSocketServerProtocolHandler.HandshakeComplete handshakeComplete =
                    (WebSocketServerProtocolHandler.HandshakeComplete) evt;
            userId = ctx.channel().attr(AttributeKey.<String>valueOf("userId")).get();
            log.info("WebSocket handshake complete: userId={}, channelId={}, requestUri={}",
                    userId, ctx.channel().id(), handshakeComplete.requestUri());
            if (userId != null) {
                WebServiceManager.addUser(userId, ctx.channel());
            }
            sendConnectionSuccessMessage(ctx);
            return;
        }

        if (evt instanceof IdleStateEvent) {
            times++;
            log.warn("WebSocket heartbeat miss: channelId={}, userId={}, missCount={}",
                    ctx.channel().id(), userId, times);
            if (times > MAX_HEARTBEAT_MISS_COUNT) {
                cleanupSocketResources(userId);
                ctx.close();
            }
            return;
        }

        super.userEventTriggered(ctx, evt);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logExceptionByType(ctx.channel().id().toString(), cause);
        cleanupSocketResources(userId);
        ctx.close();
    }

    private void sendConnectionSuccessMessage(ChannelHandlerContext ctx) {
        String welcomeMessage = String.format(
                "{\"type\":\"connection\",\"status\":\"success\",\"message\":\"WebSocket连接建立成功\",\"userId\":\"%s\",\"timestamp\":%d}",
                userId, System.currentTimeMillis());
        ctx.channel().writeAndFlush(new TextWebSocketFrame(welcomeMessage));
    }

    private void handleHeartbeat(ChannelHandlerContext ctx) {
        times = 0;
        ctx.channel().writeAndFlush(new TextWebSocketFrame(HEARTBEAT_RESPONSE_TEXT));
    }

    private void logExceptionByType(String channelId, Throwable cause) {
        if (cause instanceof IOException) {
            log.info("WebSocket network exception: channelId={}, message={}", channelId, cause.getMessage());
        } else if (cause instanceof WebSocketHandshakeException) {
            log.warn("WebSocket handshake exception: channelId={}, message={}", channelId, cause.getMessage());
        } else if (cause instanceof DecoderException || cause instanceof CorruptedFrameException) {
            log.error("WebSocket decode exception: channelId={}, message={}", channelId, cause.getMessage(), cause);
        } else if (cause instanceof IllegalArgumentException) {
            log.warn("WebSocket argument exception: channelId={}, message={}", channelId, cause.getMessage());
        } else {
            log.error("WebSocket unclassified exception: channelId={}, message={}", channelId, cause.getMessage(), cause);
        }
    }

    /**
     * Key refactor point: websocket disconnect now performs generic transport
     * cleanup only, which makes this layer independent from detection flows.
     */
    public static void cleanupSocketResources(String userId) {
        if (userId == null || userId.trim().isEmpty()) {
            return;
        }
        WebServiceManager.removeByUserId(userId);
        for (ConnectionType connectionType : ConnectionType.values()) {
            SocketManager.removeUser(userId + connectionType.getSessionTag());
        }
    }
}
