package com.usky.cdi.service.sip.device;

import com.usky.cdi.service.sip.config.SipClientProperties;
import com.usky.cdi.service.sip.model.SdpMediaInfo;
import com.usky.cdi.service.sip.push.RtspGb28181RtpSender;
import com.usky.cdi.service.sip.sdp.SdpUtils;
import com.usky.cdi.service.sip.util.NetworkUtils;
import com.usky.cdi.service.sip.util.SipDigestAuth;
import gov.nist.javax.sip.RequestEventExt;
import gov.nist.javax.sip.SipStackImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.PreDestroy;
import javax.sip.*;
import javax.sip.address.Address;
import javax.sip.address.AddressFactory;
import javax.sip.address.SipURI;
import javax.sip.header.*;
import javax.sip.message.MessageFactory;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 模拟海康摄像机 GB28181 接入：手动 REGISTER，平台点播 INVITE 后从 RTSP 推流。
 */
@Slf4j
@Service
@ConditionalOnProperty(prefix = "sip.client", name = "enabled", havingValue = "true")
public class Gb28181DeviceSimulator implements SipListener {

    private static final String GB28181_SSRC = "0100000001";

    private final SipClientProperties properties;
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(
            r -> new Thread(r, "gb28181-device"));

    private SipStack sipStack;
    private SipProvider sipProvider;
    private AddressFactory addressFactory;
    private MessageFactory messageFactory;
    private HeaderFactory headerFactory;

    private String sipTransport;
    private String localIp;
    private int localSipPort;
    private int cseq = 1;

    private final AtomicBoolean registered = new AtomicBoolean(false);
    private final AtomicBoolean registerAuthSent = new AtomicBoolean(false);
    private final AtomicReference<WWWAuthenticateHeader> authChallenge = new AtomicReference<>();
    private final AtomicReference<String> boundRtspUrl = new AtomicReference<>();
    private final AtomicReference<PendingStream> pendingStream = new AtomicReference<>();
    private volatile Future<?> streamingTask;
    private volatile ScheduledFuture<?> reRegisterTask;
    private volatile String activeDeviceId;
    /** Catalog 上报的通道 ID，IPC 默认与 deviceId 相同 */
    private volatile String activeChannelId;
    private volatile CountDownLatch registerLatch;
    private volatile String lastRegisterError;

    public Gb28181DeviceSimulator(SipClientProperties properties) {
        this.properties = properties;
    }

    @EventListener(ApplicationReadyEvent.class)
    public void onReady() {
        if (properties.isAutoStart()) {
            log.warn("sip.client.auto-start=true 已废弃，请使用 POST /api/sip/device/register?deviceId=...");
        } else {
            log.info("GB28181 设备模拟就绪，请手动调用 POST /api/sip/device/register?deviceId=... 注册");
        }
    }

    @PreDestroy
    public void destroy() {
        cancelReRegister();
        try {
            unregisterInternal(true);
        } catch (Exception e) {
            log.debug("销毁时注销失败: {}", e.getMessage());
        }
        scheduler.shutdownNow();
    }

    /**
     * 手动注册到 GB28181 平台。
     *
     * @param deviceId 20 位设备编码（动态传入）
     */
    public synchronized Map<String, Object> register(String deviceId) {
        return register(deviceId, null);
    }

    /**
     * 注册到 GB28181 平台。
     *
     * @param deviceId  20 位设备编码
     * @param channelId 可选通道编码；为空时与 deviceId 相同（单路 IPC）
     */
    public synchronized Map<String, Object> register(String deviceId, String channelId) {
        validateDeviceId(deviceId);
        String resolvedChannelId = resolveChannelId(deviceId, channelId);
        if (registered.get() && deviceId.equals(activeDeviceId)
                && resolvedChannelId.equals(activeChannelId)) {
            log.info("设备已注册: deviceId={}, channelId={}", activeDeviceId, activeChannelId);
            return status();
        }
        if (sipStack != null) {
            unregisterInternal(true);
        }
        try {
            activeDeviceId = deviceId.trim();
            activeChannelId = resolvedChannelId;
            initStack();
            registerAuthSent.set(false);
            authChallenge.set(null);
            lastRegisterError = null;
            registerLatch = new CountDownLatch(1);
            sendRegister(false, properties.getRegisterExpires());
            scheduleReRegister();
            log.info("GB28181 注册请求已发送: deviceId={}, channelId={}, platform={}@{}:{}, transport={}, passwordLen={}",
                    activeDeviceId, activeChannelId, properties.getPlatformId(),
                    properties.getServerHost(), properties.getServerPort(), sipTransport,
                    passwordLength());
            awaitRegisterResult();
        } catch (Exception e) {
            activeDeviceId = null;
            activeChannelId = null;
            shutdownStack();
            sipStack = null;
            sipProvider = null;
            throw new IllegalStateException("GB28181 注册失败: " + e.getMessage(), e);
        }
        return status();
    }

    /** 手动注销（REGISTER Expires=0）并释放 SIP 栈。 */
    public synchronized Map<String, Object> unregister() {
        unregisterInternal(true);
        return status();
    }

    private void unregisterInternal(boolean sendExpiresZero) {
        cancelReRegister();
        if (streamingTask != null) {
            streamingTask.cancel(true);
            streamingTask = null;
        }
        if (sendExpiresZero && sipStack != null && StringUtils.hasText(activeDeviceId)) {
            try {
                registerAuthSent.set(false);
                sendRegister(registered.get(), 0);
                log.info("GB28181 注销请求已发送: deviceId={}", activeDeviceId);
            } catch (Exception e) {
                log.warn("GB28181 注销请求发送失败: {}", e.getMessage());
            }
        }
        registered.set(false);
        activeDeviceId = null;
        activeChannelId = null;
        boundRtspUrl.set(null);
        pendingStream.set(null);
        shutdownStack();
        sipStack = null;
        sipProvider = null;
    }

    /**
     * 绑定 RTSP 并等待平台点播（与海康摄像机：配置好平台后，平台发 INVITE 取流）。
     */
    public Gb28181StreamResult bindRtspAndWait(String rtspUrl, int inviteWaitSeconds, int streamDurationSeconds) {
        if (!StringUtils.hasText(rtspUrl)) {
            return fail("rtspUrl 不能为空");
        }
        if (!registered.get() || !StringUtils.hasText(activeDeviceId)) {
            return fail("设备未注册，请先调用 POST /api/sip/device/register?deviceId=...");
        }
        if (sipStack == null) {
            return fail("SIP 栈未就绪，请重新注册设备");
        }

        PendingStream session = new PendingStream(rtspUrl, streamDurationSeconds);
        pendingStream.set(session);
        boundRtspUrl.set(rtspUrl);

        log.info("已绑定 RTSP，等待平台 INVITE: deviceId={}, wait={}s", activeDeviceId, inviteWaitSeconds);
        log.info(">>> 请在 GB28181 平台对设备 {} 点击「实时预览」", activeDeviceId);

        try {
            if (!session.inviteReady.await(inviteWaitSeconds, TimeUnit.SECONDS)) {
                pendingStream.compareAndSet(session, null);
                return fail("等待平台 INVITE 超时，请在平台对设备 " + activeDeviceId + " 发起实时预览");
            }
            long streamWait = streamDurationSeconds + 15L;
            if (!session.streamDone.await(streamWait, TimeUnit.SECONDS)) {
                return fail("推流超时");
            }
            return session.result != null ? session.result : fail("推流未返回结果");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return fail("等待被中断");
        }
    }

    public Map<String, Object> status() {
        Map<String, Object> map = new HashMap<>();
        map.put("registered", registered.get());
        map.put("deviceId", activeDeviceId);
        map.put("channelId", activeChannelId);
        map.put("platformId", properties.getPlatformId());
        map.put("serverHost", properties.getServerHost());
        map.put("serverPort", properties.getServerPort());
        map.put("domain", properties.getDomain());
        map.put("sipTransport", sipTransport);
        map.put("localSipAddress", localIp != null ? localIp + ":" + localSipPort : null);
        map.put("boundRtspUrl", boundRtspUrl.get());
        map.put("streaming", streamingTask != null && !streamingTask.isDone());
        map.put("lastRegisterError", lastRegisterError);
        map.put("passwordConfigured", StringUtils.hasText(properties.getPassword()));
        return map;
    }

    private void awaitRegisterResult() {
        CountDownLatch latch = registerLatch;
        if (latch == null) {
            return;
        }
        try {
            int timeout = Math.max(5, properties.getRegisterTimeout());
            if (!latch.await(timeout, TimeUnit.SECONDS)) {
                lastRegisterError = "注册超时（" + timeout + "s 内未收到平台最终响应）";
                log.warn(lastRegisterError);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            lastRegisterError = "注册等待被中断";
        }
    }

    private void completeRegisterAttempt(int status, String error) {
        if (status == Response.OK) {
            lastRegisterError = null;
        } else if (StringUtils.hasText(error)) {
            lastRegisterError = error;
        }
        CountDownLatch latch = registerLatch;
        if (latch != null) {
            latch.countDown();
        }
    }

    private int passwordLength() {
        return properties.getPassword() != null ? properties.getPassword().length() : 0;
    }

    private void initStack() throws Exception {
        SipFactory sipFactory = SipFactory.getInstance();
        sipFactory.setPathName("gov.nist");
        java.util.Properties stackProps = new java.util.Properties();
        stackProps.setProperty("javax.sip.STACK_NAME", "UskyGb28181Device");
        stackProps.setProperty("gov.nist.javax.sip.TRACE_LEVEL", "0");

        localIp = NetworkUtils.resolveLocalIp(null);
        sipTransport = normalizeTransport(properties.getSipTransport());
        stackProps.setProperty("javax.sip.OUTBOUND_PROXY",
                properties.getServerHost() + ":" + properties.getServerPort() + "/" + sipTransport);

        sipStack = sipFactory.createSipStack(stackProps);
        addressFactory = sipFactory.createAddressFactory();
        messageFactory = sipFactory.createMessageFactory();
        headerFactory = sipFactory.createHeaderFactory();

        localSipPort = findAvailablePort();
        ListeningPoint lp = sipStack.createListeningPoint(localIp, localSipPort, sipTransport);
        sipProvider = sipStack.createSipProvider(lp);
        sipProvider.addSipListener(this);
    }

    private void scheduleReRegister() {
        cancelReRegister();
        long interval = Math.max(60, (long) (properties.getRegisterExpires() * 0.85));
        reRegisterTask = scheduler.scheduleAtFixedRate(() -> {
            if (sipStack != null && StringUtils.hasText(activeDeviceId)) {
                try {
                    registerAuthSent.set(false);
                    sendRegister(registered.get(), properties.getRegisterExpires());
                } catch (Exception e) {
                    log.warn("GB28181 续注册失败: {}", e.getMessage());
                }
            }
        }, interval, interval, TimeUnit.SECONDS);
    }

    private void cancelReRegister() {
        if (reRegisterTask != null) {
            reRegisterTask.cancel(false);
            reRegisterTask = null;
        }
    }

    private void sendRegister(boolean withAuth, int expires) throws Exception {
        ensureActiveDeviceId();
        String uriUser = resolveRegisterUriUser();
        SipURI requestUri = createUri(uriUser, properties.getDomain());
        String digestUri = "sip:" + uriUser + "@" + properties.getDomain();

        Request register = buildRequest(Request.REGISTER, requestUri, activeDeviceId, "reg-tag");
        register.addHeader(headerFactory.createExpiresHeader(expires));

        if (withAuth) {
            AuthorizationHeader auth = buildAuthorization(Request.REGISTER, digestUri);
            if (auth != null) {
                register.addHeader(auth);
            }
        }

        ClientTransaction ct = sipProvider.getNewClientTransaction(register);
        ct.sendRequest();
        log.info("发送 REGISTER{} uri={} mode={} deviceId={} -> {}:{}",
                withAuth ? "(鉴权)" : "", digestUri, properties.getRegisterUriUser(), activeDeviceId,
                properties.getServerHost(), properties.getServerPort());
    }

    private String resolveRegisterUriUser() {
        if ("device".equalsIgnoreCase(properties.getRegisterUriUser())) {
            return activeDeviceId;
        }
        return properties.getPlatformId();
    }

    private void handleIncomingInvite(RequestEvent event) throws Exception {
        Request request = event.getRequest();
        PendingStream session = pendingStream.get();
        String rtspUrl = session != null ? session.rtspUrl : boundRtspUrl.get();
        if (!StringUtils.hasText(rtspUrl)) {
            sendResponse(event, Response.BUSY_HERE);
            log.warn("收到平台 INVITE 但未绑定 RTSP，请先调用 POST /api/sip/push?rtspUrl=...");
            return;
        }

        String sdpBody = extractSdp(request);
        if (!StringUtils.hasText(sdpBody)) {
            sendResponse(event, Response.BAD_REQUEST);
            failPending(fail("平台 INVITE 无 SDP"));
            return;
        }

        log.info("平台 INVITE SDP:\n{}", sdpBody);
        SdpMediaInfo media = SdpUtils.parseVideoMedia(sdpBody);
        if (media == null) {
            sendResponse(event, Response.NOT_ACCEPTABLE);
            log.warn("平台 INVITE SDP 解析失败，无法识别 video m= 行");
            failPending(fail("平台 INVITE SDP 无效"));
            return;
        }

        if (session == null) {
            session = new PendingStream(rtspUrl, properties.getDefaultDurationSeconds());
            pendingStream.set(session);
        }

        int localRtpPort = findAvailablePort();
        String remoteHost = resolveRemoteRtpHost(media, event);

        String answerSdp = SdpUtils.buildGb28181AnswerSdp(
                localIp, localRtpPort, media, activeDeviceId, GB28181_SSRC);

        log.info("应答 SDP:\n{}", answerSdp);
        Response ok = messageFactory.createResponse(Response.OK, request);
        ToHeader toHeader = (ToHeader) ok.getHeader(ToHeader.NAME);
        toHeader.setTag(generateTag());
        ok.addHeader(createContactHeader());
        Header subjectHeader = request.getHeader("Subject");
        if (subjectHeader != null) {
            ok.addHeader(subjectHeader);
        }
        ok.setContent(answerSdp, headerFactory.createContentTypeHeader("application", "sdp"));

        ServerTransaction st = event.getServerTransaction();
        if (st == null) {
            st = sipProvider.getNewServerTransaction(request);
        }
        st.sendResponse(ok);

        CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME);
        session.callId = callIdHeader != null ? callIdHeader.getCallId() : null;
        session.localRtpPort = localRtpPort;
        session.remoteRtpHost = remoteHost;
        session.remoteRtpPort = media.getPort();
        if (!StringUtils.hasText(media.getSsrc())) {
            media.setSsrc(GB28181_SSRC);
        }
        session.media = media;
        session.rtspUrl = rtspUrl;

        log.info("已应答平台 INVITE 200 OK: rtp={}:{}, protocol={}, codec={}, setup={}",
                remoteHost, media.getPort(), media.getMediaProtocol(), media.getCodec(), media.getSetup());
    }

    private String extractSdp(Request request) {
        byte[] raw = request.getRawContent();
        if (raw != null && raw.length > 0) {
            return new String(raw, StandardCharsets.UTF_8);
        }
        Object content = request.getContent();
        if (content instanceof byte[]) {
            return new String((byte[]) content, StandardCharsets.UTF_8);
        }
        return content != null ? content.toString() : null;
    }

    private String resolveRemoteRtpHost(SdpMediaInfo media, RequestEvent event) {
        String host = media.getConnectionAddress();
        if (host != null && !"0.0.0.0".equals(host)) {
            return host;
        }
        if (event instanceof RequestEventExt) {
            RequestEventExt ext = (RequestEventExt) event;
            if (StringUtils.hasText(ext.getRemoteIpAddress())) {
                return ext.getRemoteIpAddress();
            }
        }
        return properties.getServerHost();
    }

    private void handleIncomingAck(RequestEvent event) {
        PendingStream session = pendingStream.get();
        if (session == null || session.streamStarted) {
            return;
        }
        session.streamStarted = true;
        session.inviteReady.countDown();
        log.info("收到平台 ACK，开始 RTSP 推流: {}", maskUrl(session.rtspUrl));

        streamingTask = scheduler.submit(() -> {
            try {
                long packets = RtspGb28181RtpSender.stream(
                        session.rtspUrl,
                        session.media,
                        session.remoteRtpHost,
                        session.remoteRtpPort,
                        session.localRtpPort,
                        session.durationSeconds,
                        properties.getRtspTransport());
                session.result = Gb28181StreamResult.builder()
                        .success(packets > 0)
                        .rtspUrl(session.rtspUrl)
                        .callId(session.callId)
                        .localRtpPort(session.localRtpPort)
                        .remoteRtpHost(session.remoteRtpHost)
                        .remoteRtpPort(session.remoteRtpPort)
                        .packetsSent(packets)
                        .errors(packets > 0 ? new ArrayList<>() : singleError(
                                "未发出 RTP 包，请检查 RTSP 地址/网络或是否收到 IDR 关键帧"))
                        .build();
                if (packets > 0) {
                    log.info("推流完成: packets={}", packets);
                } else {
                    log.warn("推流结束但未发送 RTP 包");
                }
            } catch (Exception e) {
                log.error("推流失败: {}", e.getMessage(), e);
                session.result = fail("推流失败: " + e.getMessage());
            } finally {
                session.streamDone.countDown();
                pendingStream.compareAndSet(session, null);
            }
        });
    }

    private void handleIncomingMessage(RequestEvent event) throws Exception {
        Request request = event.getRequest();
        String body = extractSdp(request);
        if (!StringUtils.hasText(body)) {
            sendResponse(event, Response.BAD_REQUEST);
            return;
        }

        String cmdType = Gb28181CatalogBuilder.extractXmlValue(body, "CmdType");
        String sn = Gb28181CatalogBuilder.extractXmlValue(body, "SN");
        if (!StringUtils.hasText(sn)) {
            sn = "1";
        }

        sendResponse(event, Response.OK);

        if ("Catalog".equalsIgnoreCase(cmdType)) {
            String catalogXml = Gb28181CatalogBuilder.buildCatalogResponse(
                    sn, activeDeviceId, activeChannelId, properties.getChannelName());
            sendManscdpMessage(catalogXml);
            log.info("已应答 Catalog 查询: deviceId={}, channelId={}", activeDeviceId, activeChannelId);
        } else if ("DeviceInfo".equalsIgnoreCase(cmdType)) {
            String infoXml = Gb28181CatalogBuilder.buildDeviceInfoResponse(sn, activeDeviceId);
            sendManscdpMessage(infoXml);
            log.info("已应答 DeviceInfo 查询: deviceId={}", activeDeviceId);
        } else {
            log.debug("忽略未处理的 MESSAGE CmdType={}", cmdType);
        }
    }

    private void sendManscdpMessage(String xmlBody) throws Exception {
        ensureActiveDeviceId();
        SipURI requestUri = createUri(properties.getPlatformId(), properties.getDomain());
        requestUri.setPort(properties.getServerPort());
        requestUri.setTransportParam(sipTransport);

        Request message = buildRequest(Request.MESSAGE, requestUri, activeDeviceId, "msg-tag");
        message.setContent(xmlBody.getBytes(StandardCharsets.UTF_8),
                headerFactory.createContentTypeHeader("Application", "MANSCDP+xml"));

        ClientTransaction ct = sipProvider.getNewClientTransaction(message);
        ct.sendRequest();
        log.debug("发送 MESSAGE (MANSCDP+xml) -> {}:{}", properties.getServerHost(), properties.getServerPort());
    }

    private void handleIncomingBye(RequestEvent event) throws Exception {
        log.info("收到平台 BYE");
        if (streamingTask != null) {
            streamingTask.cancel(true);
        }
        sendResponse(event, Response.OK);
        PendingStream session = pendingStream.getAndSet(null);
        if (session != null) {
            session.inviteReady.countDown();
            if (session.result == null) {
                session.result = fail("平台 BYE 结束会话");
            }
            session.streamDone.countDown();
        }
    }

    private Request buildRequest(String method, SipURI requestUri, String fromUser, String fromTag) throws Exception {
        Address toAddress = addressFactory.createAddress(requestUri);
        ToHeader toHeader = headerFactory.createToHeader(toAddress, null);

        SipURI fromUri = createUri(fromUser, properties.getDomain());
        fromUri.setPort(localSipPort);
        fromUri.setTransportParam(sipTransport);
        FromHeader fromHeader = headerFactory.createFromHeader(addressFactory.createAddress(fromUri), fromTag);

        ContactHeader contactHeader = createContactHeader();

        List<ViaHeader> viaHeaders = new ArrayList<>();
        viaHeaders.add(headerFactory.createViaHeader(localIp, localSipPort, sipTransport, null));

        CSeqHeader cSeqHeader = headerFactory.createCSeqHeader(cseq++, method);
        Request request = messageFactory.createRequest(requestUri, method, sipProvider.getNewCallId(),
                cSeqHeader, fromHeader, toHeader, viaHeaders, headerFactory.createMaxForwardsHeader(70));
        request.addHeader(contactHeader);
        return request;
    }

    private ContactHeader createContactHeader() throws Exception {
        ensureActiveDeviceId();
        SipURI contactUri = createUri(activeDeviceId, properties.getDomain());
        contactUri.setHost(localIp);
        contactUri.setPort(localSipPort);
        contactUri.setTransportParam(sipTransport);
        return headerFactory.createContactHeader(addressFactory.createAddress(contactUri));
    }

    private AuthorizationHeader buildAuthorization(String method, String uri) throws Exception {
        WWWAuthenticateHeader challenge = authChallenge.get();
        if (challenge == null) {
            return null;
        }
        String cnonce = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
        String nc = "00000001";
        String qop = resolveQop(challenge.getQop());
        String response = SipDigestAuth.computeResponse(
                activeDeviceId, properties.getPassword(), challenge.getRealm(),
                method, uri, challenge.getNonce(), nc, cnonce, qop);

        AuthorizationHeader auth = headerFactory.createAuthorizationHeader("Digest");
        auth.setUsername(activeDeviceId);
        auth.setRealm(challenge.getRealm());
        auth.setNonce(challenge.getNonce());
        auth.setURI(addressFactory.createURI(uri));
        auth.setResponse(response);
        auth.setAlgorithm("MD5");
        if (qop != null && !qop.isEmpty()) {
            auth.setQop(qop);
            auth.setCNonce(cnonce);
            auth.setNonceCount(Integer.parseInt(nc, 16));
        }
        return auth;
    }

    private static String resolveQop(String qop) {
        if (qop == null || qop.isEmpty()) {
            return null;
        }
        int comma = qop.indexOf(',');
        return comma > 0 ? qop.substring(0, comma).trim() : qop.trim();
    }

    private void sendResponse(RequestEvent event, int code) throws Exception {
        Response response = messageFactory.createResponse(code, event.getRequest());
        if (code >= 200) {
            ToHeader toHeader = (ToHeader) response.getHeader(ToHeader.NAME);
            if (toHeader.getTag() == null) {
                toHeader.setTag(generateTag());
            }
        }
        ServerTransaction st = event.getServerTransaction();
        if (st == null) {
            st = sipProvider.getNewServerTransaction(event.getRequest());
        }
        st.sendResponse(response);
    }

    private SipURI createUri(String user, String domain) throws Exception {
        return addressFactory.createSipURI(user, domain);
    }

    private void failPending(Gb28181StreamResult result) {
        PendingStream session = pendingStream.getAndSet(null);
        if (session != null) {
            session.result = result;
            session.inviteReady.countDown();
            session.streamDone.countDown();
        }
    }

    private static Gb28181StreamResult fail(String msg) {
        List<String> errors = new ArrayList<>();
        errors.add(msg);
        return Gb28181StreamResult.builder().success(false).errors(errors).build();
    }

    private static List<String> singleError(String msg) {
        List<String> errors = new ArrayList<>();
        errors.add(msg);
        return errors;
    }

    private static String maskUrl(String url) {
        return url.replaceAll("://([^:]+):([^@]+)@", "://***:***@");
    }

    private static String normalizeTransport(String t) {
        return t != null && t.equalsIgnoreCase("udp") ? "udp" : "tcp";
    }

    private static int findAvailablePort() throws Exception {
        try (java.net.DatagramSocket socket = new java.net.DatagramSocket(0)) {
            int port = socket.getLocalPort();
            return port % 2 == 0 ? port : port + 1;
        }
    }

    private static String generateTag() {
        return Long.toHexString(System.nanoTime());
    }

    private void ensureActiveDeviceId() {
        if (!StringUtils.hasText(activeDeviceId)) {
            throw new IllegalStateException("设备 ID 未设置，请先注册");
        }
    }

    private static void validateDeviceId(String deviceId) {
        if (!StringUtils.hasText(deviceId)) {
            throw new IllegalArgumentException("deviceId 不能为空");
        }
        String id = deviceId.trim();
        if (id.length() != 20 || !id.chars().allMatch(Character::isDigit)) {
            throw new IllegalArgumentException("deviceId 必须为 20 位数字");
        }
    }

    private static String resolveChannelId(String deviceId, String channelId) {
        if (StringUtils.hasText(channelId)) {
            validateDeviceId(channelId);
            return channelId.trim();
        }
        return deviceId.trim();
    }

    private void shutdownStack() {
        try {
            if (sipProvider != null) {
                sipProvider.removeSipListener(this);
            }
        } catch (Exception ignored) {
        }
        if (sipStack != null) {
            try {
                ((SipStackImpl) sipStack).stop();
            } catch (Exception ignored) {
            }
        }
    }

    @Override
    public void processResponse(ResponseEvent event) {
        ClientTransaction ct = event.getClientTransaction();
        if (ct == null || !Request.REGISTER.equals(ct.getRequest().getMethod())) {
            return;
        }
        int status = event.getResponse().getStatusCode();
        if (status == Response.UNAUTHORIZED || status == Response.PROXY_AUTHENTICATION_REQUIRED) {
            WWWAuthenticateHeader challenge = (WWWAuthenticateHeader) event.getResponse().getHeader(WWWAuthenticateHeader.NAME);
            if (challenge == null) {
                challenge = (WWWAuthenticateHeader) event.getResponse().getHeader(ProxyAuthenticateHeader.NAME);
            }
            authChallenge.set(challenge);
            log.info("收到 REGISTER 401 挑战, realm={}", challenge != null ? challenge.getRealm() : "unknown");
            if (!registerAuthSent.getAndSet(true)) {
                try {
                    ExpiresHeader expires = (ExpiresHeader) ct.getRequest().getHeader(ExpiresHeader.NAME);
                    int expireSeconds = expires != null ? expires.getExpires() : properties.getRegisterExpires();
                    sendRegister(true, expireSeconds);
                } catch (Exception e) {
                    log.error("REGISTER 鉴权失败: {}", e.getMessage());
                }
            }
        } else if (status == Response.OK) {
            ExpiresHeader expires = (ExpiresHeader) ct.getRequest().getHeader(ExpiresHeader.NAME);
            if (expires != null && expires.getExpires() == 0) {
                registered.set(false);
                log.info("GB28181 注销成功: deviceId={}", activeDeviceId);
            } else {
                registered.set(true);
                log.info("GB28181 注册成功: deviceId={}（平台应可见设备在线）", activeDeviceId);
            }
            completeRegisterAttempt(status, null);
        } else if (status >= 300) {
            registered.set(false);
            String error;
            if (status == Response.FORBIDDEN) {
                error = "平台拒绝注册(403)：密码错误或设备未在平台添加，当前 passwordLen="
                        + passwordLength() + " registerUriUser=" + properties.getRegisterUriUser();
                log.warn("GB28181 注册失败 403: deviceId={}, platformId={}, domain={}, passwordLen={}。"
                        + " 请核对：① sip.client.password 与平台该设备密码一致"
                        + " ② 平台已添加设备 {} ③ domain/platformId 与海康配置一致"
                        + " ④ 仍失败可试 sip.client.register-uri-user=device",
                        activeDeviceId, properties.getPlatformId(), properties.getDomain(),
                        passwordLength(), activeDeviceId);
            } else {
                error = "平台拒绝注册(" + status + ")";
                log.warn("GB28181 注册失败: deviceId={}, status={}", activeDeviceId, status);
            }
            completeRegisterAttempt(status, error);
        }
    }

    @Override
    public void processRequest(RequestEvent event) {
        String method = event.getRequest().getMethod();
        log.info("收到 SIP 请求: {} from {}", method, formatRemote(event));
        try {
            if (Request.INVITE.equals(method)) {
                handleIncomingInvite(event);
            } else if (Request.ACK.equals(method)) {
                handleIncomingAck(event);
            } else if (Request.BYE.equals(method)) {
                handleIncomingBye(event);
            } else if (Request.MESSAGE.equals(method)) {
                handleIncomingMessage(event);
            }
        } catch (Exception e) {
            log.error("处理 {} 失败: {}", method, e.getMessage(), e);
            failPending(fail("处理 " + method + " 失败: " + e.getMessage()));
        }
    }

    private String formatRemote(RequestEvent event) {
        if (event instanceof RequestEventExt) {
            RequestEventExt ext = (RequestEventExt) event;
            return ext.getRemoteIpAddress() + ":" + ext.getRemotePort();
        }
        return properties.getServerHost();
    }

    @Override
    public void processTimeout(TimeoutEvent timeoutEvent) {
        log.warn("SIP 超时");
    }

    @Override
    public void processIOException(IOExceptionEvent exceptionEvent) {
        log.warn("SIP IO 异常: {}:{}", exceptionEvent.getHost(), exceptionEvent.getPort());
        registered.set(false);
    }

    @Override
    public void processTransactionTerminated(TransactionTerminatedEvent event) {
    }

    @Override
    public void processDialogTerminated(DialogTerminatedEvent event) {
    }

    private static final class PendingStream {
        private String rtspUrl;
        private final int durationSeconds;
        private final CountDownLatch inviteReady = new CountDownLatch(1);
        private final CountDownLatch streamDone = new CountDownLatch(1);
        private volatile boolean streamStarted;
        private volatile Gb28181StreamResult result;
        private String callId;
        private int localRtpPort;
        private String remoteRtpHost;
        private int remoteRtpPort;
        private SdpMediaInfo media;

        private PendingStream(String rtspUrl, int durationSeconds) {
            this.rtspUrl = rtspUrl;
            this.durationSeconds = durationSeconds;
        }
    }
}
