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

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.njcn.gather.detection.util.socket.cilent.NettyClient;
import com.njcn.gather.detection.util.socket.communication.model.ConnectionContext;
import com.njcn.gather.detection.util.socket.communication.model.ConnectionType;
import com.njcn.gather.detection.util.socket.config.SocketConnectionConfig;
import io.netty.channel.Channel;
import io.netty.channel.nio.NioEventLoopGroup;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Generic socket session manager.
 * Stage 4-A removes detection-only caches from this class and keeps only the
 * retained transport responsibilities: session registry, auto-connect and
 * outbound dispatch.
 *
 * @author wr
 * @author hongawen
 * @date 2026/04/07
 */
@Slf4j
@Component
public class SocketManager {

    @Resource
    private SocketConnectionConfig socketConnectionConfig;

    @Resource
    private NettyClient nettyClient;

    /**
     * Key: sessionKey(userId + connection tag), value: active channel.
     */
    private static final Map<String, Channel> SOCKET_SESSIONS = new ConcurrentHashMap<>();

    /**
     * Key: sessionKey(userId + connection tag), value: event loop group.
     */
    private static final Map<String, NioEventLoopGroup> SOCKET_GROUPS = new ConcurrentHashMap<>();

    public static void addUser(String sessionKey, Channel channel) {
        SOCKET_SESSIONS.put(sessionKey, channel);
    }

    public static void addGroup(String sessionKey, NioEventLoopGroup group) {
        SOCKET_GROUPS.put(sessionKey, group);
    }

    public static void removeUser(String sessionKey) {
        Channel channel = SOCKET_SESSIONS.remove(sessionKey);
        NioEventLoopGroup eventLoopGroup = SOCKET_GROUPS.remove(sessionKey);

        if (ObjectUtil.isNotNull(channel)) {
            try {
                channel.close().sync();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.warn("Close socket channel interrupted: sessionKey={}", sessionKey, e);
            }
        }

        if (ObjectUtil.isNotNull(eventLoopGroup)) {
            eventLoopGroup.shutdownGracefully();
            log.info("Socket connection closed: sessionKey={}", sessionKey);
        }
    }

    public static Channel getChannelByUserId(String sessionKey) {
        return SOCKET_SESSIONS.get(sessionKey);
    }

    public static NioEventLoopGroup getGroupByUserId(String sessionKey) {
        return SOCKET_GROUPS.get(sessionKey);
    }

    public static boolean isChannelActive(String sessionKey) {
        Channel channel = getChannelByUserId(sessionKey);
        return ObjectUtil.isNotNull(channel) && channel.isActive();
    }

    public static void sendMsg(String sessionKey, String msg) {
        Channel channel = SOCKET_SESSIONS.get(sessionKey);
        if (ObjectUtil.isNull(channel)) {
            log.warn("Send socket message failed because channel does not exist: sessionKey={}, message={}",
                    sessionKey, msg);
            return;
        }
        channel.writeAndFlush(msg + '\n');
        log.info("{}__{} -> {} : {}", sessionKey, channel.id(), channel.remoteAddress(), msg);
    }

    /**
     * Key refactor point: auto-connect now depends only on connection context
     * and transport callbacks attached to that context.
     */
    public void smartSend(ConnectionContext context, String msg) {
        if (ObjectUtil.isNull(context) || ObjectUtil.isNull(context.getConnectionType())) {
            log.warn("smartSend skipped because connection context is null");
            return;
        }
        String sessionKey = context.getSessionKey();
        String requestId = extractRequestId(msg);
        if (StrUtil.isBlank(sessionKey)) {
            log.warn("smartSend skipped because sessionKey is blank, requestId={}", requestId);
            return;
        }

        if (needsAutoConnect(context.getConnectionType(), requestId) && !isChannelActive(sessionKey)) {
            String ip = resolveIp(context.getConnectionType());
            Integer port = resolvePort(context.getConnectionType());
            log.info("Socket auto connect triggered: type={}, sessionKey={}, requestId={}",
                    context.getConnectionType(), sessionKey, requestId);
            CompletableFuture.runAsync(() -> nettyClient.connect(ip, port, context, msg));
            return;
        }

        sendMsg(sessionKey, msg);
    }

    private static String extractRequestId(String msg) {
        try {
            if (StrUtil.isBlank(msg)) {
                return "unknown";
            }
            JSONObject jsonObject = JSON.parseObject(msg);
            String requestId = jsonObject.getString("requestId");
            if (StrUtil.isNotBlank(requestId)) {
                return requestId;
            }
            requestId = jsonObject.getString("request_id");
            if (StrUtil.isNotBlank(requestId)) {
                return requestId;
            }
        } catch (Exception e) {
            log.debug("Extract requestId from socket message failed: {}", msg, e);
        }
        return "unknown";
    }

    private boolean needsAutoConnect(ConnectionType connectionType, String requestId) {
        if (connectionType == ConnectionType.SOURCE) {
            return SocketConnectionConfig.needsSourceConnection(requestId);
        }
        return SocketConnectionConfig.needsDeviceConnection(requestId);
    }

    private String resolveIp(ConnectionType connectionType) {
        if (connectionType == ConnectionType.SOURCE) {
            return socketConnectionConfig.getSource().getIp();
        }
        return socketConnectionConfig.getDevice().getIp();
    }

    private Integer resolvePort(ConnectionType connectionType) {
        if (connectionType == ConnectionType.SOURCE) {
            return socketConnectionConfig.getSource().getPort();
        }
        return socketConnectionConfig.getDevice().getPort();
    }
}
