package com.cmvr.edge.client.adapter;

import cmvr.api.Common;
import cmvr.api.MicPhoneServiceGrpc;
import cmvr.api.MicrophoneCommand;
import cmvr.api.SpeakerCommand;
import cmvr.api.SpeakerServiceGrpc;
import com.cmvr.edge.client.manage.GrpcServiceManager;
import com.cmvr.edge.client.utils.EdgeCommonUtil;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import io.grpc.stub.ClientCallStreamObserver;
import io.grpc.stub.ClientResponseObserver;
import io.grpc.stub.StreamObserver;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import jakarta.annotation.PreDestroy;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 实时语音对讲 gRPC 适配层。
 *
 * <p>当前机器人侧不再使用旧的 RobotAudio 双向流 proto，而是拆成两个现有服务：
 * 浏览器 -> 机器人：SpeakerService.StreamAudio 客户端流；
 * 机器人 -> 浏览器：MicPhoneService.StreamAudio 服务端流。</p>
 *
 * <p>音频格式固定为 PCM_S16LE / 48000Hz / 单声道 / 10ms / 960 bytes。
 * 所有发送到 gRPC StreamObserver 的动作都绑定到会话内单线程，避免并发 onNext 引发底层崩溃。</p>
 */
@Component
@RequiredArgsConstructor
public class RobotAudioGrpcAdapter {

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

    private static final int SAMPLE_RATE = 48000;
    private static final int CHANNELS = 1;
    private static final int BITS_PER_SAMPLE = 16;
    private static final int FRAME_MS = 10;
    private static final int FRAME_SAMPLES = SAMPLE_RATE * FRAME_MS / 1000;
    private static final int FRAME_BYTES = FRAME_SAMPLES * CHANNELS * (BITS_PER_SAMPLE / 8);
    private static final String CODEC = "PCM_S16LE";

    private static final int MAX_QUEUED_BROWSER_FRAMES = 10;
    private static final int TARGET_QUEUED_BROWSER_FRAMES = 2;
    private static final long HEARTBEAT_INTERVAL_MS = 1000L;
    private static final long HEARTBEAT_DEADLINE_MS = 2000L;

    private final GrpcServiceManager grpcServiceManager;
    private final ConcurrentMap<String, RobotAudioStream> streams = new ConcurrentHashMap<>();

    public RobotAudioStream openStream(String terminalId,
                                       String deviceId,
                                       String sessionId,
                                       String operatorId,
                                       RobotAudioStreamListener listener) {
        return openStream(terminalId, deviceId, deviceId, sessionId, operatorId, listener);
    }

    public RobotAudioStream openStream(String terminalId,
                                       String speakerDeviceId,
                                       String micDeviceId,
                                       String sessionId,
                                       String operatorId,
                                       RobotAudioStreamListener listener) {
        SpeakerServiceGrpc.SpeakerServiceStub speakerStub =
                grpcServiceManager.getGrpcClient(terminalId, SpeakerServiceGrpc.SpeakerServiceStub.class);
        MicPhoneServiceGrpc.MicPhoneServiceStub micStub =
                grpcServiceManager.getGrpcClient(terminalId, MicPhoneServiceGrpc.MicPhoneServiceStub.class);
        SpeakerServiceGrpc.SpeakerServiceBlockingStub speakerBlockingStub =
                grpcServiceManager.getGrpcClient(terminalId, SpeakerServiceGrpc.SpeakerServiceBlockingStub.class);
        MicPhoneServiceGrpc.MicPhoneServiceBlockingStub micBlockingStub =
                grpcServiceManager.getGrpcClient(terminalId, MicPhoneServiceGrpc.MicPhoneServiceBlockingStub.class);

        preflightDeviceStatus(speakerBlockingStub, micBlockingStub, speakerDeviceId, micDeviceId, sessionId);

        RobotAudioStream stream = new RobotAudioStream(
                terminalId, speakerDeviceId, micDeviceId, sessionId, operatorId, speakerStub, micStub, listener);
        RobotAudioStream exists = streams.putIfAbsent(sessionId, stream);
        if (exists != null) {
            throw new IllegalStateException("实时语音会话已存在：" + sessionId);
        }

        try {
            stream.start();
            log.info("实时对讲 gRPC 流已创建，sessionId={}，terminalId={}，speakerDeviceId={}，micDeviceId={}，operatorId={}，sampleRate={}，channels={}，bits={}，frameMs={}，frameBytes={}",
                    sessionId, terminalId, speakerDeviceId, micDeviceId, operatorId, SAMPLE_RATE, CHANNELS, BITS_PER_SAMPLE, FRAME_MS, FRAME_BYTES);
            return stream;
        } catch (RuntimeException e) {
            streams.remove(sessionId, stream);
            stream.closeLocalResources();
            throw e;
        }
    }

    @PreDestroy
    public void destroy() {
        for (RobotAudioStream stream : streams.values()) {
            try {
                stream.cancel("application shutdown");
            } catch (Exception e) {
                log.debug("关闭实时语音流异常，sessionId={}", stream.getSessionId(), e);
            }
        }
        streams.clear();
    }

    private void preflightDeviceStatus(SpeakerServiceGrpc.SpeakerServiceBlockingStub speakerStub,
                                       MicPhoneServiceGrpc.MicPhoneServiceBlockingStub micStub,
                                       String speakerDeviceId,
                                       String micDeviceId,
                                       String sessionId) {
        try {
            SpeakerCommand.GetSpeakerStateCommand.Request speakerRequest = SpeakerCommand.GetSpeakerStateCommand.Request.newBuilder()
                    .setHeader(EdgeCommonUtil.buildRequest(speakerDeviceId))
                    .build();
            SpeakerCommand.GetSpeakerStateCommand.Feedback speakerFeedback =
                    speakerStub.withDeadlineAfter(HEARTBEAT_DEADLINE_MS, TimeUnit.MILLISECONDS).getStatus(speakerRequest);
            if (speakerFeedback != null && speakerFeedback.hasHeader() && !speakerFeedback.getHeader().getSuccess()) {
                throw new IllegalStateException("speaker device unavailable: " + speakerFeedback.getHeader().getErrorMessage());
            }

            MicrophoneCommand.GetMicStateCommand.Request micRequest = MicrophoneCommand.GetMicStateCommand.Request.newBuilder()
                    .setHeader(EdgeCommonUtil.buildRequest(micDeviceId))
                    .build();
            MicrophoneCommand.GetMicStateCommand.Feedback micFeedback =
                    micStub.withDeadlineAfter(HEARTBEAT_DEADLINE_MS, TimeUnit.MILLISECONDS).getStatus(micRequest);
            if (micFeedback != null && micFeedback.hasHeader() && !micFeedback.getHeader().getSuccess()) {
                throw new IllegalStateException("microphone device unavailable: " + micFeedback.getHeader().getErrorMessage());
            }
            log.info("实时对讲设备预检通过，sessionId={}，speakerDeviceId={}，micDeviceId={}", sessionId, speakerDeviceId, micDeviceId);
        } catch (RuntimeException e) {
            log.error("实时对讲设备预检失败，sessionId={}，speakerDeviceId={}，micDeviceId={}", sessionId, speakerDeviceId, micDeviceId, e);
            throw e;
        }
    }

    public interface RobotAudioStreamListener {
        void onStarted(String sessionId, boolean accepted, String message);

        void onRobotAudio(String sessionId, long sequence, long timestampMs, byte[] pcm);

        void onHeartbeat(String sessionId, long timestampMs);

        void onRobotStop(String sessionId, String reason);

        void onError(String sessionId, Throwable throwable);

        void onCompleted(String sessionId);
    }

    public class RobotAudioStream {
        private final String terminalId;
        private final String speakerDeviceId;
        private final String micDeviceId;
        private final String sessionId;
        private final String operatorId;
        private final SpeakerServiceGrpc.SpeakerServiceStub speakerStub;
        private final MicPhoneServiceGrpc.MicPhoneServiceStub micStub;
        private final RobotAudioStreamListener listener;
        private final ScheduledExecutorService sendExecutor;
        private final ArrayBlockingQueue<byte[]> browserFrameQueue = new ArrayBlockingQueue<>(MAX_QUEUED_BROWSER_FRAMES);
        private final Object browserBufferLock = new Object();
        private final AtomicBoolean closed = new AtomicBoolean(false);
        private final AtomicBoolean terminalNotified = new AtomicBoolean(false);
        private final AtomicBoolean heartbeatInFlight = new AtomicBoolean(false);
        private final AtomicLong outboundSequence = new AtomicLong(0);
        private final AtomicLong inboundSequence = new AtomicLong(0);
        private final AtomicLong enqueued = new AtomicLong(0);
        private final AtomicLong sent = new AtomicLong(0);
        private final AtomicLong dropped = new AtomicLong(0);

        private volatile StreamObserver<SpeakerCommand.StreamSpeakerAudioCommand.Request> speakerRequestObserver;
        private volatile ClientCallStreamObserver<MicrophoneCommand.StreamMicAudioCommand.Request> micRequestObserver;
        private volatile ScheduledFuture<?> sendFuture;
        private volatile ScheduledFuture<?> heartbeatFuture;
        private volatile long lastStatsLogAt;
        private volatile byte[] pendingBrowserBytes = new byte[0];

        private RobotAudioStream(String terminalId,
                                 String speakerDeviceId,
                                 String micDeviceId,
                                 String sessionId,
                                 String operatorId,
                                 SpeakerServiceGrpc.SpeakerServiceStub speakerStub,
                                 MicPhoneServiceGrpc.MicPhoneServiceStub micStub,
                                 RobotAudioStreamListener listener) {
            this.terminalId = terminalId;
            this.speakerDeviceId = speakerDeviceId;
            this.micDeviceId = micDeviceId;
            this.sessionId = sessionId;
            this.operatorId = operatorId;
            this.speakerStub = speakerStub;
            this.micStub = micStub;
            this.listener = listener;
            this.sendExecutor = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("robot-audio-send-" + sessionId));
        }

        private void start() {
            openSpeakerStream();
            openMicStream();
            this.sendFuture = sendExecutor.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    sendOneBrowserFrame();
                }
            }, 0L, FRAME_MS, TimeUnit.MILLISECONDS);
            this.heartbeatFuture = sendExecutor.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    probeHeartbeat();
                }
            }, HEARTBEAT_INTERVAL_MS, HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS);
            listener.onStarted(sessionId, true, "audio stream started");
        }

        private void openSpeakerStream() {
            StreamObserver<SpeakerCommand.StreamSpeakerAudioCommand.Feedback> responseObserver =
                    new StreamObserver<SpeakerCommand.StreamSpeakerAudioCommand.Feedback>() {
                        @Override
                        public void onNext(SpeakerCommand.StreamSpeakerAudioCommand.Feedback feedback) {
                            if (closed.get()) {
                                return;
                            }
                            if (feedback != null && feedback.hasHeader() && !feedback.getHeader().getSuccess()) {
                                fail("speaker feedback", new IllegalStateException("speaker stream feedback failed: " + feedback.getHeader().getErrorMessage()));
                            } else {
                                listener.onHeartbeat(sessionId, System.currentTimeMillis());
                            }
                        }

                        @Override
                        public void onError(Throwable t) {
                            if (closed.get()) {
                                return;
                            }
                            fail("speaker stream", t);
                        }

                        @Override
                        public void onCompleted() {
                            if (closed.get()) {
                                return;
                            }
                            completeFromRemote();
                        }
                    };
            this.speakerRequestObserver = speakerStub.streamAudio(responseObserver);
        }

        private void openMicStream() {
            MicrophoneCommand.StreamMicAudioCommand.Request request = MicrophoneCommand.StreamMicAudioCommand.Request.newBuilder()
                    .setHeader(EdgeCommonUtil.buildRequest(micDeviceId))
                    .build();

            micStub.streamAudio(request,
                    new ClientResponseObserver<MicrophoneCommand.StreamMicAudioCommand.Request, MicrophoneCommand.StreamMicAudioCommand.Feedback>() {
                        @Override
                        public void beforeStart(ClientCallStreamObserver<MicrophoneCommand.StreamMicAudioCommand.Request> requestStream) {
                            micRequestObserver = requestStream;
                            requestStream.disableAutoInboundFlowControl();
                            log.info("麦克风音频流已启用手动入站流控，sessionId={}，micDeviceId={}", sessionId, micDeviceId);
                        }

                        @Override
                        public void onNext(MicrophoneCommand.StreamMicAudioCommand.Feedback feedback) {
                            handleMicFeedback(feedback);
                        }

                        @Override
                        public void onError(Throwable t) {
                            if (closed.get()) {
                                return;
                            }
                            fail("mic stream", t);
                        }

                        @Override
                        public void onCompleted() {
                            if (closed.get()) {
                                return;
                            }
                            completeFromRemote();
                        }
                    });
            requestNextMicFrame(1L);
        }

        /**
         * 接收浏览器 WebSocket 二进制 PCM。允许前端一次发多帧或半帧，这里按 10ms 固定帧重组。
         */
        public void sendAudio(byte[] pcm) {
            if (closed.get() || pcm == null || pcm.length == 0) {
                return;
            }
            synchronized (browserBufferLock) {
                byte[] merged = new byte[pendingBrowserBytes.length + pcm.length];
                System.arraycopy(pendingBrowserBytes, 0, merged, 0, pendingBrowserBytes.length);
                System.arraycopy(pcm, 0, merged, pendingBrowserBytes.length, pcm.length);

                int offset = 0;
                while (merged.length - offset >= FRAME_BYTES) {
                    byte[] frame = Arrays.copyOfRange(merged, offset, offset + FRAME_BYTES);
                    enqueueBrowserFrame(frame);
                    offset += FRAME_BYTES;
                }
                pendingBrowserBytes = Arrays.copyOfRange(merged, offset, merged.length);
            }
        }

        public void sendHeartbeat() {
            if (!closed.get()) {
                probeHeartbeat();
            }
        }

        public void sendStop(String reason) {
            if (!closed.compareAndSet(false, true)) {
                return;
            }
            log.info("关闭实时对讲 gRPC 流，sessionId={}，reason={}", sessionId, reason);
            sendExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    completeSpeakerStream();
                    cancelMicStream("browser stop");
                    stopTerminalAudio();
                    closeLocalResources();
                    streams.remove(sessionId, RobotAudioStream.this);
                }
            });
        }

        public void cancel(String reason) {
            if (!closed.compareAndSet(false, true)) {
                return;
            }
            log.warn("取消实时对讲 gRPC 流，sessionId={}，reason={}", sessionId, reason);
            sendExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    errorSpeakerStream(reason);
                    cancelMicStream(reason);
                    stopTerminalAudio();
                    closeLocalResources();
                    streams.remove(sessionId, RobotAudioStream.this);
                }
            });
        }

        public String getSessionId() {
            return sessionId;
        }

        public String getTerminalId() {
            return terminalId;
        }

        public String getDeviceId() {
            return speakerDeviceId;
        }

        private void enqueueBrowserFrame(byte[] frame) {
            if (browserFrameQueue.remainingCapacity() == 0) {
                int targetDrops = Math.max(1, browserFrameQueue.size() - TARGET_QUEUED_BROWSER_FRAMES + 1);
                for (int i = 0; i < targetDrops; i++) {
                    if (browserFrameQueue.poll() != null) {
                        dropped.incrementAndGet();
                    }
                }
            }
            if (browserFrameQueue.offer(frame)) {
                enqueued.incrementAndGet();
            } else {
                dropped.incrementAndGet();
            }
            logBrowserAudioStatsIfNeeded("browserAudioEnqueue");
        }

        private void sendOneBrowserFrame() {
            if (closed.get()) {
                return;
            }
            byte[] frame = browserFrameQueue.poll();
            if (frame == null) {
                return;
            }
            StreamObserver<SpeakerCommand.StreamSpeakerAudioCommand.Request> observer = speakerRequestObserver;
            if (observer == null) {
                dropped.incrementAndGet();
                return;
            }
            long sequence = outboundSequence.incrementAndGet();
            long timestampMs = System.currentTimeMillis();
            try {
                observer.onNext(buildSpeakerAudioRequest(frame, timestampMs));
                sent.incrementAndGet();
                logGrpcOutboundFormatIfNeeded(sequence, timestampMs, frame);
                logPcmStatsIfNeeded("grpc-outbound-browser", sequence, timestampMs, frame);
                logBrowserAudioStatsIfNeeded("browserAudioGrpcSend");
            } catch (Throwable t) {
                fail("speaker send", t);
            }
        }

        private SpeakerCommand.StreamSpeakerAudioCommand.Request buildSpeakerAudioRequest(byte[] frame, long timestampMs) {
            Common.AudioData audio = Common.AudioData.newBuilder()
                    .setData(ByteString.copyFrom(frame))
                    .setSampleRate(SAMPLE_RATE)
                    .setChannels(CHANNELS)
                    .setFormat(Common.AudioData.AudioFormat.PCM)
                    .setCodec(CODEC)
                    .setPts(timestampMs)
                    .setNbSamples(FRAME_SAMPLES)
                    .build();
            return SpeakerCommand.StreamSpeakerAudioCommand.Request.newBuilder()
                    .setHeader(EdgeCommonUtil.buildRequest(speakerDeviceId))
                    .setAudio(audio)
                    .build();
        }

        private void handleMicFeedback(MicrophoneCommand.StreamMicAudioCommand.Feedback feedback) {
            if (closed.get()) {
                return;
            }
            if (feedback == null) {
                listener.onHeartbeat(sessionId, System.currentTimeMillis());
                requestNextMicFrame(FRAME_MS);
                return;
            }
            if (feedback.hasHeader() && !feedback.getHeader().getSuccess()) {
                fail("mic feedback", new IllegalStateException("microphone stream feedback failed: " + feedback.getHeader().getErrorMessage()));
                return;
            }
            listener.onHeartbeat(sessionId, System.currentTimeMillis());
            if (!feedback.hasAudio()) {
                requestNextMicFrame(FRAME_MS);
                return;
            }

            Common.AudioData audio = feedback.getAudio();
            byte[] pcm = audio.getData().toByteArray();
            if (!validateInboundAudio(audio, pcm)) {
                requestNextMicFrame(FRAME_MS);
                return;
            }
            long audioDurationMs = calculateAudioDurationMs(audio, pcm);
            long sequence = inboundSequence.incrementAndGet();
            long timestampMs = audio.getPts() > 0 ? audio.getPts() : System.currentTimeMillis();
            logGrpcInboundFormatIfNeeded(sequence, timestampMs, audio, pcm);
            logPcmStatsIfNeeded("grpc-inbound-robot", sequence, timestampMs, pcm);
            byte[] normalizedPcm = normalizeInboundAudio(audio, pcm, sequence, timestampMs);
            logPcmStatsIfNeeded("webrtc-outbound-robot-normalized", sequence, timestampMs, normalizedPcm);
            try {
                listener.onRobotAudio(sessionId, sequence, timestampMs, normalizedPcm);
            } catch (Throwable t) {
                log.error("投递机器人音频到业务层失败，sessionId={}，sequence={}", sessionId, sequence, t);
                fail("business callback", t);
                return;
            }
            requestNextMicFrame(audioDurationMs);
        }

        private boolean validateInboundAudio(Common.AudioData audio, byte[] pcm) {
            if (pcm == null || pcm.length == 0) {
                return false;
            }
            if ((pcm.length & 1) != 0) {
                log.warn("机器人上行 PCM 字节数不是偶数，sessionId={}，bytes={}", sessionId, pcm.length);
                return false;
            }
            if (audio.getFormat() != Common.AudioData.AudioFormat.PCM) {
                log.warn("机器人上行音频格式不是 PCM，sessionId={}，format={}", sessionId, audio.getFormat());
                return false;
            }
            String codec = audio.getCodec();
            if (codec != null && codec.length() > 0 && !CODEC.equalsIgnoreCase(codec)) {
                log.warn("机器人上行编码不匹配，sessionId={}，codec={}，expected={}", sessionId, codec, CODEC);
            }
            return true;
        }

        private long calculateAudioDurationMs(Common.AudioData audio, byte[] pcm) {
            int sampleRate = audio.getSampleRate() <= 0 ? SAMPLE_RATE : audio.getSampleRate();
            int channels = audio.getChannels() <= 0 ? CHANNELS : audio.getChannels();
            int samples = audio.getNbSamples();
            if (samples <= 0 && pcm != null && pcm.length > 0) {
                samples = pcm.length / 2 / Math.max(1, channels);
            }
            if (sampleRate <= 0 || samples <= 0) {
                return FRAME_MS;
            }
            return Math.max(1L, Math.round(samples * 1000.0D / sampleRate));
        }

        private void requestNextMicFrame(long delayMs) {
            if (closed.get()) {
                return;
            }
            final long safeDelayMs = Math.max(1L, delayMs);
            sendExecutor.schedule(new Runnable() {
                @Override
                public void run() {
                    if (closed.get()) {
                        return;
                    }
                    ClientCallStreamObserver<MicrophoneCommand.StreamMicAudioCommand.Request> observer = micRequestObserver;
                    if (observer == null) {
                        return;
                    }
                    try {
                        observer.request(1);
                    } catch (Throwable t) {
                        fail("mic flow-control request", t);
                    }
                }
            }, safeDelayMs, TimeUnit.MILLISECONDS);
        }

        private byte[] normalizeInboundAudio(Common.AudioData audio, byte[] pcm, long sequence, long timestampMs) {
            int sourceSampleRate = audio.getSampleRate() <= 0 ? SAMPLE_RATE : audio.getSampleRate();
            int sourceChannels = audio.getChannels() <= 0 ? CHANNELS : audio.getChannels();
            if (sourceSampleRate == SAMPLE_RATE && sourceChannels == CHANNELS) {
                return pcm;
            }

            short[] monoSamples = decodeToMonoSamples(pcm, sourceChannels);
            short[] normalizedSamples = sourceSampleRate == SAMPLE_RATE
                    ? monoSamples
                    : resampleLinear(monoSamples, sourceSampleRate, SAMPLE_RATE);
            byte[] normalizedPcm = encodeMonoSamples(normalizedSamples);
            if (sequence == 1 || sequence % 500 == 0) {
                log.info("机器人上行音频已标准化，sessionId={}，sourceSampleRate={}，sourceChannels={}，targetSampleRate={}，targetChannels={}，sourceBytes={}，targetBytes={}，sequence={}，timestampMs={}",
                        sessionId, sourceSampleRate, sourceChannels, SAMPLE_RATE, CHANNELS, pcm.length, normalizedPcm.length, sequence, timestampMs);
            }
            return normalizedPcm;
        }

        private short[] decodeToMonoSamples(byte[] pcm, int sourceChannels) {
            int channels = Math.max(1, sourceChannels);
            int frameCount = pcm.length / 2 / channels;
            short[] mono = new short[frameCount];
            for (int frame = 0; frame < frameCount; frame++) {
                int sum = 0;
                for (int channel = 0; channel < channels; channel++) {
                    int sampleIndex = (frame * channels + channel) * 2;
                    int lo = pcm[sampleIndex] & 0xff;
                    int hi = pcm[sampleIndex + 1];
                    sum += (short) ((hi << 8) | lo);
                }
                mono[frame] = (short) (sum / channels);
            }
            return mono;
        }

        private short[] resampleLinear(short[] input, int sourceSampleRate, int targetSampleRate) {
            if (input.length == 0 || sourceSampleRate <= 0 || targetSampleRate <= 0) {
                return input;
            }
            int outputLength = Math.max(1, (int) Math.round(input.length * (targetSampleRate / (double) sourceSampleRate)));
            short[] output = new short[outputLength];
            double sourceStep = sourceSampleRate / (double) targetSampleRate;
            for (int i = 0; i < outputLength; i++) {
                double sourceIndex = i * sourceStep;
                int left = (int) sourceIndex;
                int right = Math.min(left + 1, input.length - 1);
                double fraction = sourceIndex - left;
                double value = input[left] + (input[right] - input[left]) * fraction;
                output[i] = clampToShort((int) Math.round(value));
            }
            return output;
        }

        private byte[] encodeMonoSamples(short[] samples) {
            byte[] pcm = new byte[samples.length * 2];
            for (int i = 0; i < samples.length; i++) {
                pcm[i * 2] = (byte) (samples[i] & 0xff);
                pcm[i * 2 + 1] = (byte) ((samples[i] >> 8) & 0xff);
            }
            return pcm;
        }

        private short clampToShort(int value) {
            if (value > Short.MAX_VALUE) {
                return Short.MAX_VALUE;
            }
            if (value < Short.MIN_VALUE) {
                return Short.MIN_VALUE;
            }
            return (short) value;
        }

        private void probeHeartbeat() {
            if (closed.get() || !heartbeatInFlight.compareAndSet(false, true)) {
                return;
            }
            MicrophoneCommand.GetMicStateCommand.Request request = MicrophoneCommand.GetMicStateCommand.Request.newBuilder()
                    .setHeader(EdgeCommonUtil.buildRequest(micDeviceId))
                    .build();
            micStub.withDeadlineAfter(HEARTBEAT_DEADLINE_MS, TimeUnit.MILLISECONDS).getStatus(request,
                    new StreamObserver<MicrophoneCommand.GetMicStateCommand.Feedback>() {
                        @Override
                        public void onNext(MicrophoneCommand.GetMicStateCommand.Feedback feedback) {
                            if (feedback != null && feedback.hasHeader() && !feedback.getHeader().getSuccess()) {
                                log.warn("实时对讲麦克风心跳返回失败，sessionId={}，message={}", sessionId, feedback.getHeader().getErrorMessage());
                                return;
                            }
                            listener.onHeartbeat(sessionId, System.currentTimeMillis());
                        }

                        @Override
                        public void onError(Throwable t) {
                            log.debug("实时对讲麦克风心跳探测失败，sessionId={}", sessionId, t);
                            heartbeatInFlight.set(false);
                        }

                        @Override
                        public void onCompleted() {
                            heartbeatInFlight.set(false);
                        }
                    });
        }

        private void fail(String source, Throwable throwable) {
            if (!closed.compareAndSet(false, true)) {
                return;
            }
            Throwable actual = throwable == null ? new RuntimeException("audio stream failed") : throwable;
            log.error("实时对讲 gRPC 流异常，source={}，sessionId={}，terminalId={}，speakerDeviceId={}，micDeviceId={}",
                    source, sessionId, terminalId, speakerDeviceId, micDeviceId, actual);
            sendExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    errorSpeakerStream(actual.getMessage());
                    cancelMicStream(actual.getMessage());
                    stopTerminalAudio();
                    closeLocalResources();
                    streams.remove(sessionId, RobotAudioStream.this);
                    if (terminalNotified.compareAndSet(false, true)) {
                        listener.onError(sessionId, actual);
                    }
                }
            });
        }

        private void completeFromRemote() {
            if (!closed.compareAndSet(false, true)) {
                return;
            }
            log.info("实时对讲 gRPC 流远端结束，sessionId={}，terminalId={}，speakerDeviceId={}，micDeviceId={}", sessionId, terminalId, speakerDeviceId, micDeviceId);
            sendExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    completeSpeakerStream();
                    cancelMicStream("remote completed");
                    closeLocalResources();
                    streams.remove(sessionId, RobotAudioStream.this);
                    if (terminalNotified.compareAndSet(false, true)) {
                        listener.onCompleted(sessionId);
                    }
                }
            });
        }

        private void completeSpeakerStream() {
            StreamObserver<SpeakerCommand.StreamSpeakerAudioCommand.Request> observer = speakerRequestObserver;
            speakerRequestObserver = null;
            if (observer != null) {
                try {
                    observer.onCompleted();
                } catch (Throwable t) {
                    log.debug("完成扬声器音频流异常，sessionId={}", sessionId, t);
                }
            }
        }

        private void errorSpeakerStream(String reason) {
            StreamObserver<SpeakerCommand.StreamSpeakerAudioCommand.Request> observer = speakerRequestObserver;
            speakerRequestObserver = null;
            if (observer != null) {
                try {
                    observer.onError(Status.CANCELLED.withDescription(reason == null ? "audio cancelled" : reason).asRuntimeException());
                } catch (Throwable t) {
                    log.debug("取消扬声器音频流异常，sessionId={}", sessionId, t);
                }
            }
        }

        private void cancelMicStream(String reason) {
            ClientCallStreamObserver<MicrophoneCommand.StreamMicAudioCommand.Request> observer = micRequestObserver;
            micRequestObserver = null;
            if (observer != null) {
                try {
                    observer.cancel(reason == null ? "audio cancelled" : reason, null);
                } catch (Throwable t) {
                    log.debug("取消麦克风音频流异常，sessionId={}", sessionId, t);
                }
            }
        }

        /**
         * 会话关闭时尽量停止终端侧播放和采集，失败不阻断本地资源释放。
         */
        private void stopTerminalAudio() {
            try {
                SpeakerCommand.StopSpeakerCommand.Request speakerStop = SpeakerCommand.StopSpeakerCommand.Request.newBuilder()
                        .setHeader(EdgeCommonUtil.buildRequest(speakerDeviceId))
                        .build();
                speakerStub.withDeadlineAfter(HEARTBEAT_DEADLINE_MS, TimeUnit.MILLISECONDS).stopPlayback(speakerStop,
                        new EmptyStreamObserver<SpeakerCommand.StopSpeakerCommand.Feedback>("stop speaker", sessionId));
            } catch (Throwable t) {
                log.debug("下发停止扬声器失败，sessionId={}", sessionId, t);
            }
            try {
                MicrophoneCommand.StopMicRecordingCommand.Request micStop = MicrophoneCommand.StopMicRecordingCommand.Request.newBuilder()
                        .setHeader(EdgeCommonUtil.buildRequest(micDeviceId))
                        .build();
                micStub.withDeadlineAfter(HEARTBEAT_DEADLINE_MS, TimeUnit.MILLISECONDS).stopRecord(micStop,
                        new EmptyStreamObserver<MicrophoneCommand.StopMicRecordingCommand.Feedback>("stop mic", sessionId));
            } catch (Throwable t) {
                log.debug("下发停止麦克风失败，sessionId={}", sessionId, t);
            }
        }

        private void closeLocalResources() {
            ScheduledFuture<?> sf = sendFuture;
            if (sf != null) {
                sf.cancel(false);
            }
            ScheduledFuture<?> hf = heartbeatFuture;
            if (hf != null) {
                hf.cancel(false);
            }
            browserFrameQueue.clear();
            pendingBrowserBytes = new byte[0];
            if (!sendExecutor.isShutdown()) {
                sendExecutor.shutdown();
            }
        }

        private void logGrpcInboundFormatIfNeeded(long sequence, long timestampMs, Common.AudioData audio, byte[] pcm) {
            if (sequence == 1 || sequence % 500 == 0) {
                log.info("gRPC机器人上行音频格式，sessionId={}，sampleRate={}，channels={}，bitsPerSample={}，encoding={}，frameSamples={}，frameMs={}，bytes={}，sequence={}，timestampMs={}",
                        sessionId,
                        audio.getSampleRate() == 0 ? SAMPLE_RATE : audio.getSampleRate(),
                        audio.getChannels() == 0 ? CHANNELS : audio.getChannels(),
                        BITS_PER_SAMPLE,
                        audio.getCodec() == null || audio.getCodec().length() == 0 ? CODEC : audio.getCodec(),
                        audio.getNbSamples() == 0 ? pcm.length / 2 : audio.getNbSamples(),
                        FRAME_MS,
                        pcm.length,
                        sequence,
                        timestampMs);
            }
        }

        private void logGrpcOutboundFormatIfNeeded(long sequence, long timestampMs, byte[] frame) {
            if (sequence == 1 || sequence % 500 == 0) {
                log.info("gRPC浏览器下行音频格式，sessionId={}，sampleRate={}，channels={}，bitsPerSample={}，encoding={}，frameSamples={}，frameMs={}，bytes={}，sequence={}，timestampMs={}",
                        sessionId, SAMPLE_RATE, CHANNELS, BITS_PER_SAMPLE, CODEC, FRAME_SAMPLES, FRAME_MS, frame.length, sequence, timestampMs);
            }
        }

        private void logBrowserAudioStatsIfNeeded(String source) {
            long now = System.currentTimeMillis();
            if (now - lastStatsLogAt < 5000L) {
                return;
            }
            lastStatsLogAt = now;
            log.info("gRPC浏览器音频统计，source={}，sessionId={}，enqueued={}，sent={}，dropped={}，queued={}",
                    source, sessionId, enqueued.get(), sent.get(), dropped.get(), browserFrameQueue.size());
        }

        private void logPcmStatsIfNeeded(String direction, long sequence, long timestampMs, byte[] pcm) {
            if (sequence != 1 && sequence % 500 != 0) {
                return;
            }
            PcmStats stats = PcmStats.from(pcm);
            log.info("PCM-STATS direction={} sessionId={} sequence={} timestampMs={} bytes={} samples={} rms={} peak={} zeroCrossRate={} firstSamples={}",
                    direction, sessionId, sequence, timestampMs, pcm.length, stats.samples, stats.rms, stats.peak, stats.zeroCrossRate, stats.firstSamples);
        }
    }

    private static class EmptyStreamObserver<T> implements StreamObserver<T> {
        private final String action;
        private final String sessionId;

        private EmptyStreamObserver(String action, String sessionId) {
            this.action = action;
            this.sessionId = sessionId;
        }

        @Override
        public void onNext(T value) {
        }

        @Override
        public void onError(Throwable t) {
            log.debug("{} failed, sessionId={}", action, sessionId, t);
        }

        @Override
        public void onCompleted() {
        }
    }

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

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

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

    private static class PcmStats {
        private final int samples;
        private final long rms;
        private final int peak;
        private final String zeroCrossRate;
        private final String firstSamples;

        private PcmStats(int samples, long rms, int peak, String zeroCrossRate, String firstSamples) {
            this.samples = samples;
            this.rms = rms;
            this.peak = peak;
            this.zeroCrossRate = zeroCrossRate;
            this.firstSamples = firstSamples;
        }

        private static PcmStats from(byte[] pcm) {
            int samples = pcm == null ? 0 : pcm.length / 2;
            if (samples == 0) {
                return new PcmStats(0, 0, 0, "0.0000", "[]");
            }
            long squareSum = 0L;
            int peak = 0;
            int zeroCross = 0;
            int previous = 0;
            StringBuilder first = new StringBuilder("[");
            for (int i = 0; i < samples; i++) {
                int lo = pcm[i * 2] & 0xff;
                int hi = pcm[i * 2 + 1];
                int value = (short) ((hi << 8) | lo);
                int abs = Math.abs(value);
                if (abs > peak) {
                    peak = abs;
                }
                squareSum += (long) value * value;
                if (i > 0 && ((previous < 0 && value >= 0) || (previous >= 0 && value < 0))) {
                    zeroCross++;
                }
                previous = value;
                if (i < 8) {
                    if (i > 0) {
                        first.append(',');
                    }
                    first.append(value);
                }
            }
            first.append(']');
            long rms = (long) Math.sqrt(squareSum / (double) samples);
            String zcr = String.format("%.4f", zeroCross / (double) samples);
            return new PcmStats(samples, rms, peak, zcr, first.toString());
        }
    }

}
