package cn.skcks.docking.gb28181.wvp.service.gb28181;

import cn.hutool.core.date.*;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import cn.skcks.docking.gb28181.common.json.JsonException;
import cn.skcks.docking.gb28181.common.json.JsonResponse;
import cn.skcks.docking.gb28181.common.json.JsonUtils;
import cn.skcks.docking.gb28181.common.redis.RedisUtil;
import cn.skcks.docking.gb28181.core.sip.gb28181.cache.CacheUtil;
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
import cn.skcks.docking.gb28181.media.config.ZlmMediaConfig;
import cn.skcks.docking.gb28181.media.dto.rtp.CloseRtpServer;
import cn.skcks.docking.gb28181.media.dto.rtp.GetRtpInfoResp;
import cn.skcks.docking.gb28181.media.dto.rtp.OpenRtpServer;
import cn.skcks.docking.gb28181.media.dto.rtp.OpenRtpServerResp;
import cn.skcks.docking.gb28181.media.dto.status.ResponseStatus;
import cn.skcks.docking.gb28181.media.proxy.ZlmMediaService;
import cn.skcks.docking.gb28181.sdp.GB28181Description;
import cn.skcks.docking.gb28181.sdp.GB28181SDPBuilder;
import cn.skcks.docking.gb28181.sdp.media.MediaStreamMode;
import cn.skcks.docking.gb28181.service.record.vo.RecordInfoItemVO;
import cn.skcks.docking.gb28181.service.ssrc.SsrcService;
import cn.skcks.docking.gb28181.wvp.config.MediaRtmpConfig;
import cn.skcks.docking.gb28181.wvp.config.ProxySipConfig;
import cn.skcks.docking.gb28181.wvp.config.WvpProxyConfig;
import cn.skcks.docking.gb28181.wvp.executor.DefaultVideoExecutor;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
import cn.skcks.docking.gb28181.wvp.service.record.RecordInfoService;
import cn.skcks.docking.gb28181.wvp.service.record.dto.RecordInfoDTO;
import cn.skcks.docking.gb28181.wvp.service.video.VideoService;
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
import cn.skcks.docking.gb28181.wvp.sip.response.SipResponseBuilder;
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
import cn.skcks.docking.gb28181.wvp.utils.RetryUtil;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import gov.nist.javax.sdp.MediaDescriptionImpl;
import gov.nist.javax.sdp.fields.TimeField;
import gov.nist.javax.sdp.fields.URIField;
import gov.nist.javax.sip.message.SIPRequest;
import gov.nist.javax.sip.message.SIPResponse;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.async.DeferredResult;

import javax.sdp.Connection;
import javax.sdp.SdpFactory;
import javax.sdp.TimeDescription;
import javax.sip.header.CallIdHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Vector;
import java.util.concurrent.*;

@Slf4j
@Service
@RequiredArgsConstructor
public class Gb28181DownloadService {
    private final ZlmMediaService zlmMediaService;
    private final ZlmMediaConfig zlmMediaConfig;
    private final SsrcService ssrcService;
    private final DeviceService deviceService;
    private final DockingService dockingService;
    private final ProxySipConfig proxySipConfig;
    private final SipSender sender;
    private final SipSubscribe subscribe;
    private final VideoService videoService;
    private final WvpProxyConfig wvpProxyConfig;
    private final RecordInfoService recordInfoService;
    private final MediaRtmpConfig mediaRtmpConfig;

    @Qualifier(DefaultVideoExecutor.EXECUTOR_BEAN_NAME)
    private final Executor executor;

    private final ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(64);

    private final ConcurrentMap<String, DeferredResult<JsonResponse<String>>> requestMap = new ConcurrentHashMap<>();

    private final RealtimeManager realtimeManager;

    @Getter
    private final RealTime realtime = new RealTime();

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class VideoInfo {
        private String streamId;
        private String url;
        private String callId;
        private WvpProxyDevice device;
    }

    public void header(HttpServletResponse response) {
        response.setContentType("video/mp4");
        response.setHeader("Accept-Ranges", "none");
        response.setHeader("Connection", "close");
    }

    public void streamHeader(HttpServletResponse response) {
        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);

        response.setHeader("Accept-Ranges", "none");
        response.setHeader("Connection", "close");
    }

    public void header(HttpServletResponse response, String fileName) {
        header(response);
        response.setHeader("Content-Disposition",
                MessageFormat.format("attachment; filename=\"{0}.mp4\"",fileName));
    }

    private String videoRtmpUrl(String streamId) {
        String rtmpSchema = "rtmp://" + zlmMediaConfig.getIp() + ":" + mediaRtmpConfig.getRtmpPort();
        return StringUtils.joinWith("/", rtmpSchema, "rtp", streamId);
    }

    private String videoWsUrl(String rtmpUrl){
        String rtmpSchema = "rtmp://" + zlmMediaConfig.getIp() + ":" + mediaRtmpConfig.getRtmpPort();
        if(StringUtils.isNotBlank(proxySipConfig.getProxyMediaUrl())){
            return StringUtils.replace(rtmpUrl + ".live.flv", rtmpSchema, proxySipConfig.getProxyMediaUrl());
        } else {
            String wsSchema = StringUtils.replace(zlmMediaConfig.getUrl(), "http://", "ws://");
            return StringUtils.replace(rtmpUrl + ".live.flv", rtmpSchema, wsSchema);
        }
    }

    @SneakyThrows
    private void writeErrorToResponse(HttpServletResponse response, JsonResponse<?> json) {
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        IoUtil.writeUtf8(response.getOutputStream(), false, json);
    }

    private int openRtpServer(String streamId, int streamMode) {
        GetRtpInfoResp rtpInfo = zlmMediaService.getRtpInfo(streamId);
        if (rtpInfo.getExist()) {
            log.warn("流 {} 已存在", streamId);
            return 0;
        }

        OpenRtpServer openRtpServer = new OpenRtpServer();
        openRtpServer.setPort(0);
        openRtpServer.setStreamId(streamId);
        openRtpServer.setTcpMode(streamMode);
        OpenRtpServerResp openRtpServerResp = zlmMediaService.openRtpServer(openRtpServer);
        log.info("openRtpServerResp => {}", openRtpServerResp);
        if (!openRtpServerResp.getCode().equals(ResponseStatus.Success)) {
            log.error("{}", openRtpServerResp.getCode().getMsg());
            return -1;
        }
        return openRtpServerResp.getPort();
    }

    @SneakyThrows
    @SuppressWarnings({"UnstableApiUsage", "unchecked"})
    public void video(HttpServletRequest request, HttpServletResponse response, String deviceCode, Date startTime, Date endTime, Boolean fileHeader, Boolean useDownload) {
        AsyncContext asyncContext = request.startAsync();
        asyncContext.setTimeout(DateUtil.between(startTime, DateUtil.offsetSecond(endTime, 60), DateUnit.MS));
        asyncContext.start(()->{
            DateTime start = DateUtil.date();
            HttpServletResponse asyncResponse = (HttpServletResponse)asyncContext.getResponse();
            try{
                if(proxySipConfig.isUseRecordInfoQueryBeforeDownload()){
                    String name = MessageFormat.format("{0} {1}-{2}", deviceCode, startTime, endTime);
                    Retryer<JsonResponse<List<RecordInfoItemVO>>> retryer = RetryerBuilder.<JsonResponse<List<RecordInfoItemVO>>>newBuilder()
                            // 异常就重试
                            .retryIfException()
                            .retryIfRuntimeException()
                            // 重试间隔
                            .withWaitStrategy(WaitStrategies.fixedWait(proxySipConfig.getRetryRecordInfoQueryBeforeDownloadInterval(), proxySipConfig.getRetryRecordInfoQueryBeforeDownloadIntervalUnit()))
                            // 重试次数
                            .withStopStrategy(StopStrategies.stopAfterAttempt(proxySipConfig.getRetryRecordInfoQueryBeforeDownloadTimes()))
                            .retryIfResult((result) -> {
                                log.info("{}", result);
                                return result == null ||
                                        result.getCode() != Response.OK ||
                                        result.getData() == null ||
                                        result.getData().isEmpty();
                            })
                            .withRetryListener(RetryUtil.defaultRetryListener(name)).build();

                    retryer.call(()->{
                        CompletableFuture<JsonResponse<List<RecordInfoItemVO>>> future = new CompletableFuture<>();
                        future.completeOnTimeout(JsonResponse.error("录像查询超时"),1,TimeUnit.MINUTES);
                        // 发起设备录像查询
                        DeferredResult<JsonResponse<List<RecordInfoItemVO>>> requestedRecordInfo =
                                recordInfoService.requestRecordInfo(new RecordInfoDTO(deviceCode, startTime, endTime, "", 0, "all"));

                        requestedRecordInfo.setResultHandler(result -> {
                            future.complete((JsonResponse<List<RecordInfoItemVO>>) result);
                        });
                        requestedRecordInfo.onError((throwable)->{
                            future.complete(JsonResponse.error(throwable.getMessage()));
                        });

                        return future.get();
                    });
                }

                download(deviceCode, startTime, endTime, useDownload).whenComplete((videoInfo, e) -> {
                    writeFileHeader(response, deviceCode, startTime, endTime, fileHeader);
                    log.info("videoInfo {}", videoInfo);
                    if (e != null) {
                        writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
                    } else if (videoInfo == null) {
                        writeErrorToResponse(asyncResponse, JsonResponse.error("下载失败"));
                    } else if (wvpProxyConfig.getUseFfmpeg()) {
                        executor.execute(()-> {
                            videoService.ffmpegRecord(asyncResponse, videoInfo.getUrl(), DateUtil.between(startTime, endTime, DateUnit.SECOND), videoInfo.getDevice(), videoInfo.getCallId());

                            DateTime end = DateUtil.date();
                            asyncContext.complete();
                            log.info("下载总耗时: {}, deviceCode {}, startTime {}, endTime {}", DateUtil.between(start,end, DateUnit.SECOND), deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
                        });
                    } else {
                        videoService.javaCVrecord(asyncResponse, videoInfo.getUrl(), DateUtil.between(startTime, endTime, DateUnit.SECOND) + 15);
                    }
                });
            } catch(Exception e) {
                writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
            } finally {
                if(!wvpProxyConfig.getUseFfmpeg()){
                    DateTime end = DateUtil.date();
                    asyncContext.complete();
                    log.info("下载总耗时: {}, deviceCode {}, startTime {}, endTime {}", DateUtil.between(start,end, DateUnit.SECOND), deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
                }
            }
        });
    }

    @SneakyThrows
    public DeferredResult<JsonResponse<String>> videoUrl(String deviceCode, Date startTime, Date endTime) {
        long time = DateUtil.between(startTime, endTime, DateUnit.MS);
        DeferredResult<JsonResponse<String>> result = new DeferredResult<>(time);
        result.onTimeout(()->{
            result.setResult(JsonResponse.error("请求超时"));
        });
        WvpProxyDevice device = deviceService.getDeviceByDeviceCode(deviceCode).orElse(null);
        if(device == null){
            result.setResult(JsonResponse.error("设备不存在"));
            return result;
        }
        WvpProxyDocking docking = dockingService.getDeviceByDeviceCode(device.getGbDeviceId()).orElse(null);
        if(docking == null){
            result.setResult(JsonResponse.error("设备(通道)不存在"));
            return result;
        }

        closeExistRequest(deviceCode);
        requestMap.put(deviceCode, result);

        // 间隔一定时间(200ms) 给设备足够的时间结束前次请求
        scheduledExecutorService.schedule(()->{
            download(deviceCode, startTime, endTime).whenComplete((videoInfo, e)->{
                log.info("获取媒体信息 {}", videoInfo);
                String cacheKey = CacheUtil.getKey(docking.getGbDeviceId(), device.getGbDeviceChannelId());
                String existCallId = RedisUtil.StringOps.get(cacheKey);
                // 到达时间后主动结束, 防止某些设备不会主动结束
                scheduledExecutorService.schedule(()->{
                    log.info("到达结束时间 发送 bye 关闭 {} {}", videoInfo.getDevice().getGbDeviceChannelId(), videoInfo.getCallId());
                    String deviceIp = docking.getIp();
                    int devicePort = Integer.parseInt(docking.getPort());
                    if(StringUtils.isNotBlank(existCallId)) {
                        sender.sendRequest((provider, localIp, localPort) ->
                                SipRequestBuilder.createByeRequest(deviceIp, devicePort, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, existCallId));
                    }
                    RedisUtil.KeyOps.delete(cacheKey);
                    zlmMediaService.closeRtpServer(CloseRtpServer.builder()
                            .streamId(videoInfo.streamId)
                            .build());
                }, time, TimeUnit.MILLISECONDS);

                String url = videoWsUrl(videoInfo.getUrl());
                url = StringUtils.replaceOnce(url, ".live.flv", ".live.mp4");
                result.setResult(JsonResponse.success(url));
            });
        }, 200, TimeUnit.MILLISECONDS);
        return result;
    }

    @SneakyThrows
    public synchronized DeferredResult<JsonResponse<String>> realtimeVideoUrl(String deviceCode){
        DeferredResult<JsonResponse<String>> result = new DeferredResult<>(TimeUnit.SECONDS.toMillis(60));
        result.onTimeout(()->{
            result.setResult(JsonResponse.error("请求超时"));
        });
        WvpProxyDevice device = deviceService.getDeviceByDeviceCode(deviceCode).orElse(null);
        if(device == null){
            result.setResult(JsonResponse.error("设备不存在"));
            return result;
        }
        WvpProxyDocking docking = dockingService.getDeviceByDeviceCode(device.getGbDeviceId()).orElse(null);
        if(docking == null){
            result.setResult(JsonResponse.error("设备(通道)不存在"));
            return result;
        }

        String existUrl = RedisUtil.StringOps.get(CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), deviceCode));
        if(Optional.ofNullable(existUrl).isPresent()){
            result.setResult(JsonResponse.success(existUrl));
            return result;
        }
        // 间隔一定时间(200ms) 给设备足够的时间结束前次请求
        scheduledExecutorService.schedule(()->{
            realtime.realtime(deviceCode).whenComplete((videoInfo, e)->{
                if(videoInfo == null){
                    result.setResult(JsonResponse.error("媒体信息获取失败"));
                    return;
                }
                log.info("获取媒体信息 {}", videoInfo);

                // 原始链接转换为前端可用的链接
                RedisUtil.StringOps.set(CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), videoInfo.getCallId()), JsonUtils.toJson(videoInfo));
                String url = videoWsUrl(videoInfo.getUrl());
                videoInfo.setUrl(url);

                realtimeManager.addPlaying(deviceCode, videoInfo);
                result.setResult(JsonResponse.success(url));
            });
        }, 200, TimeUnit.MILLISECONDS);
        return result;
    }

    private void closeExistRequest(String deviceCode) {
        requestMap.computeIfPresent(deviceCode,(key,requestResult)->{
            log.info("关闭已存在的视频请求 {}", deviceCode);
            if(!requestResult.hasResult()){
                requestResult.setResult(JsonResponse.error("同一设备重复请求, 本次请求结束"));
                realtimeManager.removePlaying(deviceCode);
            }
            return null;
        });
    }

    public void closeRealtimeVideoNow(String deviceCode){
        realtimeManager.removePlaying(deviceCode);
    }


    @SneakyThrows
    public void videoStream(HttpServletRequest request, HttpServletResponse response, String deviceCode, Date startTime, Date endTime) {
        AsyncContext asyncContext = request.startAsync();
        asyncContext.setTimeout(0);
        asyncContext.start(()->{
            DateTime start = DateUtil.date();
            HttpServletResponse asyncResponse = (HttpServletResponse)asyncContext.getResponse();
            try{
                download(deviceCode, startTime,endTime).whenComplete((videoInfo, e)->{
                    streamHeader(asyncResponse);
                    if(e != null){
                        writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
                    } else if(videoInfo == null){
                        writeErrorToResponse(asyncResponse, JsonResponse.error("下载失败"));
                    } else if(wvpProxyConfig.getUseFfmpeg()){
                        executor.execute(()->{
                            videoService.ffmpegRecord(asyncResponse, videoInfo.getUrl(), DateUtil.between(startTime,endTime,DateUnit.SECOND),videoInfo.getDevice(),videoInfo.getCallId());

                            DateTime end = DateUtil.date();
                            asyncContext.complete();
                            log.info("下载总耗时: {}, deviceCode {}, startTime {}, endTime {}", DateUtil.between(start,end, DateUnit.SECOND), deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
                        });
                    } else {
                        videoService.javaCVrecord(asyncResponse, videoInfo.getUrl(), DateUtil.between(startTime,endTime,DateUnit.SECOND) + 15);
                    }
                });
            } catch(Exception e) {
                writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));

            } finally {
                if(!wvpProxyConfig.getUseFfmpeg()){
                    DateTime end = DateUtil.date();
                    asyncContext.complete();
                    log.info("下载总耗时: {}, deviceCode {}, startTime {}, endTime {}", DateUtil.between(start,end, DateUnit.SECOND), deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
                }
            }
        });
    }

    private void writeFileHeader(HttpServletResponse response, String deviceCode, Date startTime, Date endTime, Boolean fileHeader){
        if(fileHeader){
            header(response, StringUtils.joinWith("_",
                    deviceCode,
                    DateUtil.format(startTime, DatePattern.PURE_DATETIME_FORMAT),
                    DateUtil.format(endTime, DatePattern.PURE_DATETIME_FORMAT)));
        } else {
            header(response);
        }
    }

    private  class RealTime {
        @SneakyThrows
        public CompletableFuture<VideoInfo> realtime(String deviceCode) {
            Optional<WvpProxyDevice> deviceByDeviceCode = deviceService.getDeviceByDeviceCode(deviceCode);
            if (deviceByDeviceCode.isEmpty()) {
                String reason = MessageFormat.format("未能找到 设备编码 为 {0} 的设备", deviceCode);
                log.error("{}",reason);
                throw new JsonException(reason);
            } else {
                WvpProxyDevice device = deviceByDeviceCode.get();
                return realtime(device.getGbDeviceId(), device.getGbDeviceChannelId());
            }
        }

        @SneakyThrows
        public CompletableFuture<VideoInfo> realtime(String gbDeviceId, String channel){
            CompletableFuture<VideoInfo> result = new CompletableFuture<>();
            Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(gbDeviceId);
            if(deviceByGbDeviceId.isEmpty()){
                log.info("未能找到 国标编码 {} 的注册信息", gbDeviceId);
                result.complete(null);
                return result;
            }
            Optional<WvpProxyDevice> deviceByGbDeviceIdAndChannel = deviceService.getDeviceByGbDeviceIdAndChannel(gbDeviceId, channel);
            if (deviceByGbDeviceIdAndChannel.isEmpty()) {
                log.info("未能找到 编码 {}, 通道 {} 的设备", gbDeviceId, channel);
                result.complete(null);
                return result;
            }

            WvpProxyDevice device = deviceByGbDeviceIdAndChannel.get();
            WvpProxyDocking docking = deviceByGbDeviceId.get();

            String streamId = GB28181SDPBuilder.getStreamId(gbDeviceId, channel, IdUtil.getSnowflakeNextIdStr());
            int isTcp = proxySipConfig.getStreamMode() == MediaStreamMode.UDP ? 0 : 1;
            MediaStreamMode streamMode = proxySipConfig.getStreamMode();
            String ip = zlmMediaConfig.getIp();
            int port = openRtpServer(streamId, isTcp);
            if(port <= 0){
                log.error("zlm 暂无可用端口");
                result.complete(null);
                return result;
            }
            String ssrc = ssrcService.getPlaySsrc();
            GB28181Description gb28181Description = GB28181SDPBuilder.Receiver.play(gbDeviceId, channel, Connection.IP4, ip, port, ssrc, streamMode);
            sender.sendRequest(inviteRequest(docking, device, gb28181Description, ssrc, streamId, result));
            return result;
        }


        @SneakyThrows
        public SipSender.SendRequest inviteRequest(WvpProxyDocking docking, WvpProxyDevice device, GB28181Description description, String ssrc, String streamId, CompletableFuture<VideoInfo> result) {
            String cacheKey = CacheUtil.getKey(docking.getGbDeviceId(), device.getGbDeviceChannelId());
            String existCallId = RedisUtil.StringOps.get(cacheKey);

            // 限制单个设备/通道 只能 点播 一路
            if(StringUtils.isNotBlank(existCallId)){
                String deviceIp = docking.getIp();
                int devicePort = Integer.parseInt(docking.getPort());
                sender.sendRequest((provider,localIp,localPort)->
                        SipRequestBuilder.createByeRequest(deviceIp, devicePort, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, existCallId));
                RedisUtil.KeyOps.delete(cacheKey);
                log.info("关闭已存在的 点播请求 {} {}", cacheKey, existCallId);
                Thread.sleep(500);
            }

            return (provider, ip, port) -> {
                CallIdHeader callId = provider.getNewCallId();
                String subscribeKey = GenericSubscribe.Helper.getKey(Request.INVITE, callId.getCallId());
                subscribe.getInviteSubscribe().addPublisher(subscribeKey);
                Flow.Subscriber<SIPResponse> subscriber = inviteSubscriber(docking,device,subscribeKey,cacheKey, ssrc, streamId, result,  0, TimeUnit.SECONDS);
                subscribe.getInviteSubscribe().addSubscribe(subscribeKey, subscriber);
                RedisUtil.StringOps.set(cacheKey, callId.getCallId());
                return SipRequestBuilder.createInviteRequest(ip, port, docking, device.getGbDeviceChannelId(), description.toString(), SipUtil.generateViaTag(), SipUtil.generateFromTag(), null, ssrc, callId);
            };
        }
    }

    public CompletableFuture<VideoInfo> download(String deviceCode, Date startTime, Date endTime) {
        return download(deviceCode,startTime,endTime, proxySipConfig.isUsePlaybackToDownload());
    }

    @SneakyThrows
    public CompletableFuture<VideoInfo> download(String deviceCode, Date startTime, Date endTime, Boolean useDownload) {
        Optional<WvpProxyDevice> deviceByDeviceCode = deviceService.getDeviceByDeviceCode(deviceCode);
        if (deviceByDeviceCode.isEmpty()) {
            String reason = MessageFormat.format("未能找到 设备编码 为 {0} 的设备", deviceCode);
            log.error("{}",reason);
            throw new JsonException(reason);
        } else {
            WvpProxyDevice device = deviceByDeviceCode.get();
            return download(device.getGbDeviceId(), device.getGbDeviceChannelId(), startTime, endTime, useDownload);
        }
    }

    @SneakyThrows
    public CompletableFuture<VideoInfo> download(String gbDeviceId, String channel, Date startTime, Date endTime, Boolean useDownload){
        CompletableFuture<VideoInfo> result = new CompletableFuture<>();
        Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(gbDeviceId);
        long time = DateUtil.between(startTime, endTime, DateUnit.SECOND);
        if(deviceByGbDeviceId.isEmpty()){
            log.info("未能找到 国标编码 {} 的注册信息", gbDeviceId);
            result.complete(null);
            return result;
        }
        Optional<WvpProxyDevice> deviceByGbDeviceIdAndChannel = deviceService.getDeviceByGbDeviceIdAndChannel(gbDeviceId, channel);
        if (deviceByGbDeviceIdAndChannel.isEmpty()) {
            log.info("未能找到 编码 {}, 通道 {} 的设备", gbDeviceId, channel);
            result.complete(null);
            return result;
        }

        WvpProxyDevice device = deviceByGbDeviceIdAndChannel.get();
        WvpProxyDocking docking = deviceByGbDeviceId.get();

        ZoneId zoneId = ZoneId.of(GB28181Constant.TIME_ZONE);
        long start =  LocalDateTimeUtil.of(startTime.toInstant(), zoneId).atZone(zoneId).toEpochSecond();
        long end =  LocalDateTimeUtil.of(endTime.toInstant(), zoneId).atZone(zoneId).toEpochSecond();
        String streamId = GB28181SDPBuilder.getStreamId(gbDeviceId, channel, String.valueOf(start), String.valueOf(end), IdUtil.getSnowflakeNextIdStr());
        int isTcp = proxySipConfig.getStreamMode() == MediaStreamMode.UDP ? 0 : 1;
        MediaStreamMode streamMode = proxySipConfig.getStreamMode();
        String ip = zlmMediaConfig.getIp();
        int port = openRtpServer(streamId, isTcp);
        if(port <= 0){
            log.error("zlm 暂无可用端口");
            result.complete(null);
            return result;
        }

        String ssrc = ssrcService.getPlaySsrc();
        TimeField timeField = new TimeField();
        timeField.setStartTime(start);
        timeField.setStopTime(end);
        TimeDescription timeDescription = SdpFactory.getInstance().createTimeDescription(timeField);
        GB28181SDPBuilder.Action action = GB28181SDPBuilder.Action.DOWNLOAD;
        if(useDownload == null ? proxySipConfig.isUsePlaybackToDownload(): !useDownload){
            action = GB28181SDPBuilder.Action.PLAY_BACK;
        }
        GB28181Description gb28181Description = GB28181SDPBuilder.Receiver.build(action, gbDeviceId, channel, Connection.IP4, ip, port, ssrc, streamMode, timeDescription);
        gb28181Description.setSessionName(SdpFactory.getInstance().createSessionName(action.getAction()));
        gb28181Description.setTimeDescriptions(new Vector<>(){{add(timeDescription);}});
        MediaDescriptionImpl media = (MediaDescriptionImpl) gb28181Description.getMediaDescriptions(true).get(0);
        if(proxySipConfig.getStreamMode() != MediaStreamMode.UDP){
            media.getMedia().setProtocol("RTP/AVP/TCP");
        }
        if(useDownload == null ? !proxySipConfig.isUsePlaybackToDownload(): useDownload){
            media.setAttribute("downloadspeed", String.valueOf(4));
        }
        URIField uriField = new URIField();
        uriField.setURI(StringUtils.joinWith(":", channel, "0"));
        gb28181Description.setURI(uriField);
        sender.sendRequest(inviteRequest(docking, device, gb28181Description, action, ssrc, streamId, result, time));
        return result;
    }

    @SneakyThrows
    public SipSender.SendRequest inviteRequest(WvpProxyDocking docking, WvpProxyDevice device, GB28181Description description, GB28181SDPBuilder.Action action, String ssrc, String streamId, CompletableFuture<VideoInfo> result, long time) {
        String cacheKey = CacheUtil.getKey(docking.getGbDeviceId(), device.getGbDeviceChannelId());
        String existCallId = RedisUtil.StringOps.get(cacheKey);

        // 限制单个设备/通道 只能 点播 一路
        if(StringUtils.isNotBlank(existCallId)){
            String deviceIp = docking.getIp();
            int devicePort = Integer.parseInt(docking.getPort());
            sender.sendRequest((provider,localIp,localPort)->
                    SipRequestBuilder.createByeRequest(deviceIp, devicePort, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, existCallId));
            RedisUtil.KeyOps.delete(cacheKey);
            log.info("关闭已存在的 点播请求 {} {}", cacheKey, existCallId);
            Thread.sleep(500);
        }

        return (provider, ip, port) -> {
            CallIdHeader callId = provider.getNewCallId();
            String subscribeKey = GenericSubscribe.Helper.getKey(Request.INVITE, callId.getCallId());
            subscribe.getInviteSubscribe().addPublisher(subscribeKey);
            Flow.Subscriber<SIPResponse> subscriber = inviteSubscriber(docking,device,subscribeKey,cacheKey, ssrc, streamId, result, time + 60, TimeUnit.SECONDS);
            subscribe.getInviteSubscribe().addSubscribe(subscribeKey, subscriber);
            RedisUtil.StringOps.set(cacheKey, callId.getCallId());
            // 用以 提前 启动 ffmpeg 预备录制, 需要配置 ffmpeg rw_timeout 时长 避免收不到流
            result.completeAsync(() -> new VideoInfo(streamId, videoRtmpUrl(streamId), callId.getCallId(), device), executor);
            return SipRequestBuilder.createInviteRequest(ip, port, docking, device.getGbDeviceChannelId(), description.toString(), SipUtil.generateViaTag(), SipUtil.generateFromTag(), null, ssrc, callId);
        };
    }

    public Flow.Subscriber<SIPResponse> inviteSubscriber(WvpProxyDocking docking, WvpProxyDevice device, String subscribeKey, String cacheKey, String ssrc,String streamId,CompletableFuture<VideoInfo> result, long time, TimeUnit unit){
        ScheduledFuture<?>[] schedule = new ScheduledFuture<?>[1];
        Flow.Subscriber<SIPResponse> subscriber = new Flow.Subscriber<>() {
            private Flow.Subscription subscription;
            @Override
            public void onSubscribe(Flow.Subscription subscription) {
                this.subscription = subscription;
                log.info("订阅 {} {}", MessageProcessor.Method.INVITE, subscribeKey);
                subscription.request(1);
            }

            @Override
            public void onNext(SIPResponse item) {
                int statusCode = item.getStatusCode();
                log.debug("{} 收到订阅消息 {}", subscribeKey, item);
                if (statusCode == Response.TRYING) {
                    log.info("订阅 {} {} 尝试连接流媒体服务", MessageProcessor.Method.INVITE, subscribeKey);
                    subscription.request(1);
                } else if (statusCode >= Response.OK && statusCode < Response.MULTIPLE_CHOICES) {
                    log.info("订阅 {} {} 流媒体服务连接成功, 开始推送视频流", MessageProcessor.Method.INVITE, subscribeKey);
                    log.info("收到响应状态 {}", statusCode);
                    String callId = item.getCallId().getCallId();

                    scheduledExecutorService.schedule(()->{
                        sender.sendRequest(((provider, ip, port) -> {
                            String fromTag = item.getFromTag();
                            String toTag = item.getToTag();
                            String key = GenericSubscribe.Helper.getKey(Request.BYE, callId);
                            subscribe.getByeSubscribe().addPublisher(key);
                            subscribe.getByeSubscribe().addSubscribe(key, byeSubscriber(key, device, cacheKey, streamId, time, unit));
                            return SipRequestBuilder.createAckRequest(Response.OK, ip, port, docking, device.getGbDeviceChannelId(), fromTag, toTag, callId);
                        }));
                    },200,TimeUnit.MILLISECONDS);
                } else {
                    log.info("订阅 {} {} 连接流媒体服务时出现异常, 终止订阅", MessageProcessor.Method.INVITE, subscribeKey);
                    zlmMediaService.closeRtpServer(new CloseRtpServer(streamId));
                    result.complete(null);
                    ssrcService.releaseSsrc(zlmMediaConfig.getId(), ssrc);
                    onComplete();
                }
            }

            @Override
            public void onError(Throwable throwable) {
                onComplete();
            }

            @Override
            public void onComplete() {
                subscribe.getInviteSubscribe().delPublisher(subscribeKey);
                schedule[0].cancel(true);
            }
        };

        if(time == 0){
            schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, 60, unit);
        } else {
            schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, time, unit);
        }
        return subscriber;
    }

    public Flow.Subscriber<SIPRequest> byeSubscriber(String key,WvpProxyDevice device, String cacheKey,String streamId, long time, TimeUnit unit){
        ScheduledFuture<?>[] schedule = new ScheduledFuture<?>[1];
        Flow.Subscriber<SIPRequest> subscriber = new Flow.Subscriber<>() {
            @Override
            public void onSubscribe(Flow.Subscription subscription) {
                log.info("创建订阅 {}", key);
                subscription.request(1);
            }

            @SneakyThrows
            @Override
            public void onNext(SIPRequest request) {
                RedisUtil.KeyOps.delete(cacheKey);
                String transport = request.getTopmostViaHeader().getTransport();
                String ip = request.getLocalAddress().getHostAddress();
                if(time <= 0) {
                    String callId = request.getCallId().getCallId();
                    String infoKey = CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), callId);
                    VideoInfo parse = JsonUtils.parse(RedisUtil.StringOps.get(infoKey), VideoInfo.class);
                    Optional.ofNullable(parse).ifPresent((info)->{
                        if(info.getCallId().equalsIgnoreCase(callId)){
                            realtimeManager.removePlaying(device.getDeviceCode());
                        }
                    });
                    RedisUtil.KeyOps.delete(infoKey);
                }

                sender.getProvider(transport,ip)
                        .sendResponse(SipResponseBuilder.response(request, Response.OK, "OK"));
                onComplete();
            }

            @Override
            public void onError(Throwable throwable) {
                throwable.printStackTrace();
                onComplete();
            }

            @Override
            public void onComplete() {
                subscribe.getByeSubscribe().delPublisher(key);
                if(time > 0){
                    schedule[0].cancel(true);
                }
                zlmMediaService.closeRtpServer(new CloseRtpServer(streamId));
            }
        };

        if(time > 0){
            schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, time, unit);
        }
        return subscriber;
    }
}
