package com.cmvr.edge.client.service;

import com.cmvr.common.utils.uuid.IdUtils;
import com.cmvr.edge.client.adapter.RobotAudioGrpcAdapter;
import com.cmvr.edge.client.adapter.RobotAudioGrpcAdapter.RobotAudioStream;
import com.cmvr.edge.client.adapter.RobotAudioGrpcAdapter.RobotAudioStreamListener;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

/**
 * 实时语音对讲业务层。
 *
 * 分工：
 * 1. WebSocket 只处理 WebRTC 信令 JSON，不承载音频；
 * 2. 浏览器 WebRTC 媒体通道产出的 PCM 通过 sendBrowserAudio 方法进入业务层；
 * 3. 业务层通过 RobotAudioGrpcAdapter 将 PCM 转发到机器人 gRPC 双向流；
 * 4. 机器人音频通过 gRPC 回调给业务层，再交给已接入的 WebRTC 媒体发送侧。
 */
@Service
@RequiredArgsConstructor
public class AudioService implements RobotAudioStreamListener {

    private static final Logger log = LoggerFactory.getLogger(AudioService.class);

    /** 机器人心跳超时时间：机器人每 1 秒上报一次，超过 5 秒判定离线。 */
    private static final long ROBOT_HEARTBEAT_TIMEOUT_MS = 5000L;
    private static final int BROWSER_AUDIO_FRAME_BYTES = 960;
    private static final int BROWSER_AUDIO_FRAME_MS = 10;
    private static final int TARGET_BROWSER_AUDIO_QUEUE_FRAMES = 4;
    private static final int MAX_BROWSER_AUDIO_CATCH_UP_FRAMES = 4;
    private static final int WARN_BROWSER_AUDIO_QUEUE_FRAMES = 100;
    private static final long BROWSER_AUDIO_PUSH_INTERVAL_MS = 5L;

    private final RobotAudioGrpcAdapter robotAudioGrpcAdapter;

    private final ObjectMapper objectMapper = new ObjectMapper();

    /** sessionId -> 会话上下文。 */
    private final ConcurrentHashMap<String, AudioSession> sessionMap = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, String> webSocketSessionMap = new ConcurrentHashMap<>();

    /**
     * 处理 /ws/signaling 收到的 JSON 信令。
     */
    public void handleSignalingMessage(WebSocketSession webSocketSession, String payload) {
        try {
            JsonNode jsonNode = objectMapper.readTree(payload);
            String type = getText(jsonNode, "type");
            if (type == null) {
                sendError(webSocketSession, null, "信令缺少type字段");
                return;
            }

            switch (type) {
                case "join":
                case "start":
                    handleJoin(webSocketSession, jsonNode);
                    break;
                case "offer":
                case "answer":
                case "candidate":
                    sendError(webSocketSession, getText(jsonNode, "sessionId"), "WebRTC disabled, use WebSocket binary audio");
                    break;
                case "leave":
                case "stop":
                    stopSession(getText(jsonNode, "sessionId"), "browser stop");
                    break;
                case "heartbeat":
                    handleBrowserHeartbeat(webSocketSession, jsonNode);
                    break;
                default:
                    sendError(webSocketSession, getText(jsonNode, "sessionId"), "未知信令类型：" + type);
                    break;
            }
        } catch (Exception e) {
            log.error("处理WebRTC信令失败，payload={}", payload, e);
            sendError(webSocketSession, null, "处理信令失败：" + e.getMessage());
        }
    }

    /**
     * 浏览器 WebSocket 断开时关闭对应会话。
     */
    public void handleWebSocketClosed(WebSocketSession webSocketSession) {
        if (webSocketSession == null) {
            return;
        }
        String wsId = webSocketSession.getId();
        webSocketSessionMap.remove(wsId);
        for (AudioSession session : sessionMap.values()) {
            if (session.webSocketSession != null && wsId.equals(session.webSocketSession.getId())) {
                stopSession(session.sessionId, "websocket closed");
            }
        }
    }

    /**
     * 浏览器通过 WebSocket BinaryMessage 发送 PCM_S16LE / 48k / mono 音频。
     * 推荐前端按 10ms 一帧发送：480 samples，960 bytes。
     */
    public void handleBrowserAudioMessage(WebSocketSession webSocketSession, ByteBuffer payload) {
        if (webSocketSession == null || payload == null) {
            return;
        }
        String sessionId = webSocketSessionMap.get(webSocketSession.getId());
        if (sessionId == null) {
            sessionId = findSessionIdByWebSocket(webSocketSession);
        }
        if (sessionId == null) {
            log.warn("收到浏览器音频但未找到会话，wsSessionId={}，bytes={}", webSocketSession.getId(), payload.remaining());
            return;
        }
        byte[] pcm = new byte[payload.remaining()];
        payload.get(pcm);
        sendBrowserAudio(sessionId, pcm);
    }

    /**
     * WebRTC 媒体层调用此方法，把浏览器侧 PCM-48K-16bit-单声道音频帧发给机器人。
     *
     * 注意：WebSocket 禁止传音频，音频必须来自 WebRTC 媒体通道。
     */
    public void sendBrowserAudio(String sessionId, byte[] pcm) {
        AudioSession session = sessionMap.get(sessionId);
        if (session == null) {
            log.warn("浏览器音频帧找不到会话，sessionId={}", sessionId);
            return;
        }
        try {
            session.lastBrowserAudioAt = System.currentTimeMillis();
            session.robotStream.sendAudio(pcm);
        } catch (Exception e) {
            log.error("发送浏览器音频到机器人失败，sessionId={}", sessionId, e);
            failSession(session, "发送浏览器音频失败：" + e.getMessage(), true);
        }
    }

    /**
     * WebRTC 媒体发送层可注册该回调，接收机器人 gRPC 上传的 PCM 音频。
     */
    public void registerRobotAudioConsumer(String sessionId, Consumer<byte[]> robotAudioConsumer) {
        AudioSession session = sessionMap.get(sessionId);
        if (session == null) {
            throw new IllegalArgumentException("会话不存在：" + sessionId);
        }
        session.robotAudioConsumer = robotAudioConsumer;
    }

    public void unregisterRobotAudioConsumer(String sessionId) {
        AudioSession session = sessionMap.get(sessionId);
        if (session != null) {
            session.robotAudioConsumer = null;
        }
    }

    private void handleJoin(WebSocketSession webSocketSession, JsonNode jsonNode) {
        String terminalId = getText(jsonNode, "terminalId");
        String deviceId = getText(jsonNode, "deviceId");
        String speakerDeviceId = getText(jsonNode, "speakerDeviceId");
        String micDeviceId = getText(jsonNode, "micDeviceId");
        String operatorId = getText(jsonNode, "operatorId");
        String sessionId = getText(jsonNode, "sessionId");

        if (speakerDeviceId == null) {
            speakerDeviceId = deviceId;
        }
        if (micDeviceId == null) {
            micDeviceId = deviceId;
        }

        if (terminalId == null || speakerDeviceId == null || micDeviceId == null) {
            sendError(webSocketSession, sessionId, "terminalId、speakerDeviceId、micDeviceId不能为空");
            return;
        }
        if (deviceId == null) {
            deviceId = speakerDeviceId;
        }
        if (sessionId == null) {
            sessionId = IdUtils.fastSimpleUUID();
        }

        try {
            AudioSession exists = sessionMap.get(sessionId);
            if (exists != null) {
                exists.webSocketSession = webSocketSession;
                exists.lastBrowserSignalAt = System.currentTimeMillis();
                webSocketSessionMap.put(webSocketSession.getId(), sessionId);
                sendState(exists, "joined");
                return;
            }

            AudioSession session = new AudioSession();
            session.sessionId = sessionId;
            session.terminalId = terminalId;
            session.deviceId = deviceId;
            session.speakerDeviceId = speakerDeviceId;
            session.micDeviceId = micDeviceId;
            session.operatorId = operatorId;
            session.webSocketSession = webSocketSession;
            session.state = AudioSessionState.CONNECTING;
            session.createdAt = System.currentTimeMillis();
            session.lastBrowserSignalAt = session.createdAt;
            session.lastRobotHeartbeatAt = session.createdAt;
            startBrowserAudioPusher(session);

            sessionMap.put(sessionId, session);
            webSocketSessionMap.put(webSocketSession.getId(), sessionId);
            try {
                session.robotStream = robotAudioGrpcAdapter.openStream(terminalId, speakerDeviceId, micDeviceId, sessionId, operatorId, this);
            } catch (Exception e) {
                sessionMap.remove(sessionId);
                webSocketSessionMap.remove(webSocketSession.getId());
                stopBrowserAudioPusher(session);
                throw e;
            }
            sendState(session, "joined");
            log.info("实时语音会话创建成功，sessionId={}，terminalId={}，speakerDeviceId={}，micDeviceId={}", sessionId, terminalId, speakerDeviceId, micDeviceId);
        } catch (Exception e) {
            log.error("创建实时语音会话失败，sessionId={}", sessionId, e);
            sendError(webSocketSession, sessionId, "创建实时语音会话失败：" + e.getMessage());
        }
    }

    private void handleWebRtcSignal(WebSocketSession webSocketSession, JsonNode jsonNode, String type) {
        String sessionId = getText(jsonNode, "sessionId");
        
        // 如果sessionId为空，尝试从WebSocket会话关联的session中查找
        if (sessionId == null) {
            for (AudioSession session : sessionMap.values()) {
                if (session.webSocketSession != null && 
                    session.webSocketSession.getId().equals(webSocketSession.getId())) {
                    sessionId = session.sessionId;
                    log.warn("WebRTC信令缺少sessionId，自动关联到会话: {}", sessionId);
                    break;
                }
            }
        }
        
        AudioSession session = sessionMap.get(sessionId);
        if (session == null) {
            sendError(webSocketSession, sessionId, "会话不存在，请先发送join信令");
            return;
        }

        session.webSocketSession = webSocketSession;
        session.lastBrowserSignalAt = System.currentTimeMillis();

        try {
            if ("offer".equals(type)) {
                String sdp = getSdp(jsonNode);
                if (sdp == null) {
                    sendError(webSocketSession, sessionId, "offer信令缺少sdp字段");
                    return;
                }
                sendError(webSocketSession, sessionId, "WebRTC disabled, use WebSocket binary audio");
            } else if ("candidate".equals(type)) {
                IceCandidateValue candidate = getIceCandidate(jsonNode);
                if (candidate == null || candidate.candidate == null) {
                    sendError(webSocketSession, sessionId, "candidate信令格式错误");
                    return;
                }
                sendError(webSocketSession, sessionId, "WebRTC disabled, use WebSocket binary audio");
            } else {
                ObjectNode ack = baseMessage("signalAck", sessionId);
                ack.put("signalType", type);
                ack.put("state", session.state.name());
                sendJson(webSocketSession, ack);
            }
        } catch (Exception e) {
            log.error("处理WebRTC媒体信令失败，sessionId={}，type={}", sessionId, type, e);
            sendError(webSocketSession, sessionId, "处理WebRTC媒体信令失败：" + e.getMessage());
        }
    }

    private void handleBrowserHeartbeat(WebSocketSession webSocketSession, JsonNode jsonNode) {
        String sessionId = getText(jsonNode, "sessionId");
        AudioSession session = sessionMap.get(sessionId);
        if (session == null) {
            sendError(webSocketSession, sessionId, "会话不存在");
            return;
        }
        session.lastBrowserSignalAt = System.currentTimeMillis();
        if (session.robotStream != null) {
            session.robotStream.sendHeartbeat();
        }

        ObjectNode pong = baseMessage("heartbeatAck", sessionId);
        pong.put("state", session.state.name());
        sendJson(webSocketSession, pong);
    }

    public void stopSession(String sessionId, String reason) {
        if (sessionId == null) {
            return;
        }
        AudioSession session = sessionMap.remove(sessionId);
        if (session == null) {
            return;
        }
        try {
            session.state = AudioSessionState.CLOSED;
            removeWebSocketMapping(session);
            stopBrowserAudioPusher(session);
            if (session.robotStream != null) {
                session.robotStream.sendStop(reason);
            }
            sendState(session, "closed");
            log.info("实时语音会话已关闭，sessionId={}，reason={}", sessionId, reason);
        } catch (Exception e) {
            log.error("关闭实时语音会话异常，sessionId={}", sessionId, e);
        }
    }

    /**
     * 定时检测机器人心跳。
     */
    @Scheduled(fixedDelay = 1000L)
    public void checkRobotHeartbeatTimeout() {
        long now = System.currentTimeMillis();
        Iterator<Map.Entry<String, AudioSession>> iterator = sessionMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, AudioSession> entry = iterator.next();
            AudioSession session = entry.getValue();
            if (session.state == AudioSessionState.CLOSED || session.state == AudioSessionState.FAILED) {
                continue;
            }
            if (now - session.lastRobotHeartbeatAt > ROBOT_HEARTBEAT_TIMEOUT_MS) {
                log.warn("机器人心跳超时，sessionId={}，terminalId={}，speakerDeviceId={}，micDeviceId={}",
                        session.sessionId, session.terminalId, session.speakerDeviceId, session.micDeviceId);
                // 先从业务会话表摘除，避免 cancel/close 触发的异步回调再次进入同一个会话并重复关闭 native 对象。
                iterator.remove();
                session.state = AudioSessionState.ROBOT_OFFLINE;
                sendState(session, "robotOffline");
                stopBrowserAudioPusher(session);
                if (session.robotStream != null) {
                    session.robotStream.cancel("robot heartbeat timeout");
                }
                removeWebSocketMapping(session);
            }
        }
    }

    @Override
    public void onStarted(String sessionId, boolean accepted, String message) {
        AudioSession session = sessionMap.get(sessionId);
        if (session == null) {
            return;
        }
        session.lastRobotHeartbeatAt = System.currentTimeMillis();
        session.state = accepted ? AudioSessionState.RUNNING : AudioSessionState.FAILED;
        ObjectNode event = baseMessage("robotStartAck", sessionId);
        event.put("accepted", accepted);
        event.put("message", message == null ? "" : message);
        event.put("state", session.state.name());
        sendJson(session.webSocketSession, event);
        if (!accepted) {
            failSession(session, message == null ? "机器人拒绝开启语音对讲" : message, true);
        }
    }

    @Override
    public void onRobotAudio(String sessionId, long sequence, long timestampMs, byte[] pcm) {
        AudioSession session = sessionMap.get(sessionId);
        if (session == null) {
//            log.warn("机器人音频帧找不到会话，sessionId={}", sessionId);
            return;
        }
        session.lastRobotAudioAt = System.currentTimeMillis();
        session.lastRobotHeartbeatAt = session.lastRobotAudioAt;
        enqueueRobotAudioForBrowser(session, pcm);
        Consumer<byte[]> consumer = session.robotAudioConsumer;
        if (consumer != null) {
            try {
                consumer.accept(pcm);
            } catch (Exception e) {
                log.error("投递机器人音频到WebRTC媒体层失败，sessionId={}，sequence={}", sessionId, sequence, e);
            }
        } else {
            log.debug("未注册机器人音频消费者，丢弃PCM帧，sessionId={}，sequence={}，bytes={}",
                    sessionId, sequence, pcm == null ? 0 : pcm.length);
        }
    }

    @Override
    public void onHeartbeat(String sessionId, long timestampMs) {
        AudioSession session = sessionMap.get(sessionId);
        if (session == null) {
            return;
        }
        session.lastRobotHeartbeatAt = System.currentTimeMillis();
        if (session.state == AudioSessionState.CONNECTING) {
            session.state = AudioSessionState.RUNNING;
            sendState(session, "running");
        }
    }

    @Override
    public void onRobotStop(String sessionId, String reason) {
        AudioSession session = sessionMap.get(sessionId);
        if (session != null) {
            sendState(session, "robotStop");
        }
        stopSession(sessionId, reason == null ? "robot stop" : reason);
    }

    @Override
    public void onError(String sessionId, Throwable throwable) {
        AudioSession session = sessionMap.get(sessionId);
        if (session == null) {
            return;
        }
        failSession(session, throwable == null ? "机器人音频流异常" : throwable.getMessage(), false);
    }

    @Override
    public void onCompleted(String sessionId) {
        AudioSession session = sessionMap.get(sessionId);
        if (session != null) {
            session.state = AudioSessionState.CLOSED;
            sendState(session, "grpcCompleted");
        }
        sessionMap.remove(sessionId);
        stopBrowserAudioPusher(session);
        removeWebSocketMapping(session);
    }

    private void failSession(AudioSession session, String reason, boolean cancelRobotStream) {
        sessionMap.remove(session.sessionId, session);
        session.state = AudioSessionState.FAILED;
        stopBrowserAudioPusher(session);
        ObjectNode event = baseMessage("error", session.sessionId);
        event.put("message", reason == null ? "未知错误" : reason);
        event.put("state", session.state.name());
        sendJson(session.webSocketSession, event);
        if (cancelRobotStream && session.robotStream != null) {
            try {
                session.robotStream.cancel(reason == null ? "audio session failed" : reason);
            } catch (Exception e) {
                log.debug("取消机器人音频流异常，sessionId={}", session.sessionId, e);
            }
        }
        removeWebSocketMapping(session);
    }

    private void sendState(AudioSession session, String type) {
        ObjectNode event = baseMessage(type, session.sessionId);
        event.put("terminalId", session.terminalId);
        event.put("deviceId", session.deviceId);
        event.put("speakerDeviceId", session.speakerDeviceId);
        event.put("micDeviceId", session.micDeviceId);
        event.put("state", session.state.name());
        event.put("createdAt", session.createdAt);
        event.put("lastRobotHeartbeatAt", session.lastRobotHeartbeatAt);
        sendJson(session.webSocketSession, event);
    }

    private void sendError(WebSocketSession session, String sessionId, String message) {
        ObjectNode event = baseMessage("error", sessionId);
        event.put("message", message == null ? "未知错误" : message);
        sendJson(session, event);
    }

    private ObjectNode baseMessage(String type, String sessionId) {
        ObjectNode event = objectMapper.createObjectNode();
        event.put("type", type);
        if (sessionId != null) {
            event.put("sessionId", sessionId);
        }
        event.put("timestamp", System.currentTimeMillis());
        return event;
    }

    private void sendJson(WebSocketSession session, ObjectNode event) {
        sendText(session, toJson(event));
    }

    private void startBrowserAudioPusher(AudioSession session) {
        if (session == null || session.robotBrowserAudioExecutor != null) {
            return;
        }
        session.robotBrowserAudioExecutor = Executors.newSingleThreadScheduledExecutor(
                new NamedThreadFactory("robot-browser-audio-" + session.sessionId));
        session.robotBrowserAudioFuture = session.robotBrowserAudioExecutor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    int queued = session.robotBrowserAudioQueue.size();
                    int framesToSend = queued > TARGET_BROWSER_AUDIO_QUEUE_FRAMES
                            ? Math.min(MAX_BROWSER_AUDIO_CATCH_UP_FRAMES, queued - TARGET_BROWSER_AUDIO_QUEUE_FRAMES + 1)
                            : 1;
                    for (int i = 0; i < framesToSend; i++) {
                        byte[] frame = session.robotBrowserAudioQueue.poll();
                        if (frame == null) {
                            break;
                        }
                        sendBinary(session.webSocketSession, frame);
                        session.robotBrowserAudioSent++;
                    }
                    logRobotBrowserAudioStatsIfNeeded(session, "robotAudioPush");
                } catch (Exception e) {
                    log.error("推送机器人音频到浏览器失败，sessionId={}", session.sessionId, e);
                }
            }
        }, 0L, BROWSER_AUDIO_PUSH_INTERVAL_MS, TimeUnit.MILLISECONDS);
    }

    private void enqueueRobotAudioForBrowser(AudioSession session, byte[] pcm) {
        if (session == null || pcm == null || pcm.length == 0) {
            return;
        }
        synchronized (session.robotBrowserAudioLock) {
            byte[] merged = new byte[session.robotBrowserRemainder.length + pcm.length];
            System.arraycopy(session.robotBrowserRemainder, 0, merged, 0, session.robotBrowserRemainder.length);
            System.arraycopy(pcm, 0, merged, session.robotBrowserRemainder.length, pcm.length);

            int offset = 0;
            while (merged.length - offset >= BROWSER_AUDIO_FRAME_BYTES) {
                byte[] frame = new byte[BROWSER_AUDIO_FRAME_BYTES];
                System.arraycopy(merged, offset, frame, 0, BROWSER_AUDIO_FRAME_BYTES);
                offerRobotAudioFrame(session, frame);
                offset += BROWSER_AUDIO_FRAME_BYTES;
            }

            int remain = merged.length - offset;
            session.robotBrowserRemainder = new byte[remain];
            if (remain > 0) {
                System.arraycopy(merged, offset, session.robotBrowserRemainder, 0, remain);
            }
        }
        logRobotBrowserAudioStatsIfNeeded(session, "robotAudioEnqueue");
    }

    private void offerRobotAudioFrame(AudioSession session, byte[] frame) {
        session.robotBrowserAudioQueue.offer(frame);
        session.robotBrowserAudioEnqueued++;
    }

    private void stopBrowserAudioPusher(AudioSession session) {
        if (session == null) {
            return;
        }
        ScheduledFuture<?> future = session.robotBrowserAudioFuture;
        if (future != null) {
            future.cancel(false);
            session.robotBrowserAudioFuture = null;
        }
        ScheduledExecutorService executor = session.robotBrowserAudioExecutor;
        if (executor != null) {
            executor.shutdownNow();
            session.robotBrowserAudioExecutor = null;
        }
        synchronized (session.robotBrowserAudioLock) {
            session.robotBrowserAudioQueue.clear();
            session.robotBrowserRemainder = new byte[0];
        }
    }

    private void logRobotBrowserAudioStatsIfNeeded(AudioSession session, String source) {
        long now = System.currentTimeMillis();
        if (now - session.lastRobotBrowserAudioLogAt < 5000L) {
            return;
        }
        session.lastRobotBrowserAudioLogAt = now;
        int queued = session.robotBrowserAudioQueue.size();
        log.info("机器人到浏览器音频统计，source={}，sessionId={}，enqueued={}，sent={}，dropped={}，queued={}，remainderBytes={}",
                source,
                session.sessionId,
                session.robotBrowserAudioEnqueued,
                session.robotBrowserAudioSent,
                session.robotBrowserAudioDropped,
                queued,
                session.robotBrowserRemainder.length);
        if (queued > WARN_BROWSER_AUDIO_QUEUE_FRAMES) {
            log.warn("机器人到浏览器音频队列积压较高，sessionId={}，queued={}，estimatedDelayMs={}，请检查前端播放消费或网络发送是否跟不上",
                    session.sessionId, queued, queued * BROWSER_AUDIO_FRAME_MS);
        }
    }

    private void sendBinary(WebSocketSession session, byte[] pcm) {
        if (session == null || !session.isOpen() || pcm == null || pcm.length == 0) {
            return;
        }
        try {
            synchronized (session) {
                session.sendMessage(new BinaryMessage(pcm));
            }
        } catch (IOException e) {
            log.error("发送WebSocket二进制音频失败，wsSessionId={}", session.getId(), e);
        }
    }

    private void sendText(WebSocketSession session, String text) {
        if (session == null || !session.isOpen()) {
            return;
        }
        try {
            synchronized (session) {
                session.sendMessage(new TextMessage(text));
            }
        } catch (IOException e) {
            log.error("发送WebSocket信令失败", e);
        }
    }

    private String toJson(ObjectNode event) {
        try {
            return objectMapper.writeValueAsString(event);
        } catch (Exception e) {
            throw new RuntimeException("序列化WebSocket信令失败", e);
        }
    }

    private String findSessionIdByWebSocket(WebSocketSession webSocketSession) {
        if (webSocketSession == null) {
            return null;
        }
        String wsId = webSocketSession.getId();
        for (AudioSession session : sessionMap.values()) {
            if (session.webSocketSession != null && wsId.equals(session.webSocketSession.getId())) {
                webSocketSessionMap.put(wsId, session.sessionId);
                return session.sessionId;
            }
        }
        return null;
    }

    private void removeWebSocketMapping(AudioSession session) {
        if (session != null && session.webSocketSession != null) {
            webSocketSessionMap.remove(session.webSocketSession.getId(), session.sessionId);
        }
    }

    private String getText(JsonNode node, String fieldName) {
        JsonNode value = node == null ? null : node.get(fieldName);
        if (value == null || value.isNull()) {
            return null;
        }
        String text = value.asText();
        return text == null || text.trim().isEmpty() ? null : text.trim();
    }

    private String getSdp(JsonNode jsonNode) {
        String direct = getText(jsonNode, "sdp");
        if (direct != null) {
            return direct;
        }
        JsonNode description = jsonNode == null ? null : jsonNode.get("description");
        return getText(description, "sdp");
    }

    private IceCandidateValue getIceCandidate(JsonNode jsonNode) {
        JsonNode candidateNode = jsonNode == null ? null : jsonNode.get("candidate");
        if (candidateNode == null || candidateNode.isTextual()) {
            IceCandidateValue value = new IceCandidateValue();
            value.candidate = candidateNode == null ? getText(jsonNode, "candidate") : candidateNode.asText();
            value.sdpMid = getText(jsonNode, "sdpMid");
            value.sdpMLineIndex = getInt(jsonNode, "sdpMLineIndex", 0);
            return value;
        }

        IceCandidateValue value = new IceCandidateValue();
        value.candidate = getText(candidateNode, "candidate");
        value.sdpMid = getText(candidateNode, "sdpMid");
        value.sdpMLineIndex = getInt(candidateNode, "sdpMLineIndex", 0);
        return value;
    }

    private int getInt(JsonNode node, String fieldName, int defaultValue) {
        JsonNode value = node == null ? null : node.get(fieldName);
        return value == null || !value.isNumber() ? defaultValue : value.asInt(defaultValue);
    }

    private enum AudioSessionState {
        CONNECTING,
        RUNNING,
        ROBOT_OFFLINE,
        CLOSED,
        FAILED
    }

    /**
     * 单个实时对讲会话上下文。
     */
    private static class AudioSession {
        private String sessionId;
        private String terminalId;
        private String deviceId;
        private String speakerDeviceId;
        private String micDeviceId;
        private String operatorId;
        private volatile WebSocketSession webSocketSession;
        private volatile RobotAudioStream robotStream;
        private volatile Consumer<byte[]> robotAudioConsumer;
        private volatile ScheduledExecutorService robotBrowserAudioExecutor;
        private volatile ScheduledFuture<?> robotBrowserAudioFuture;
        private final LinkedBlockingQueue<byte[]> robotBrowserAudioQueue = new LinkedBlockingQueue<>();
        private final Object robotBrowserAudioLock = new Object();
        private byte[] robotBrowserRemainder = new byte[0];
        private volatile long robotBrowserAudioEnqueued;
        private volatile long robotBrowserAudioSent;
        private volatile long robotBrowserAudioDropped;
        private volatile long lastRobotBrowserAudioLogAt;
        private volatile AudioSessionState state;
        private volatile long createdAt;
        private volatile long lastBrowserSignalAt;
        private volatile long lastBrowserAudioAt;
        private volatile long lastRobotAudioAt;
        private volatile long lastRobotHeartbeatAt;
    }

    private static class IceCandidateValue {
        private String candidate;
        private String sdpMid;
        private int sdpMLineIndex;
    }

    private static class NamedThreadFactory implements ThreadFactory {
        private final String name;

        private NamedThreadFactory(String name) {
            this.name = name;
        }

        @Override
        public Thread newThread(Runnable runnable) {
            Thread thread = new Thread(runnable, name);
            thread.setDaemon(true);
            return thread;
        }
    }
}
