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

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
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.core.sip.gb28181.constant.GB28181Constant;
import cn.skcks.docking.gb28181.core.sip.gb28181.sdp.GB28181Description;
import cn.skcks.docking.gb28181.core.sip.gb28181.sdp.MediaSdpHelper;
import cn.skcks.docking.gb28181.core.sip.gb28181.sdp.StreamMode;
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.service.ssrc.SsrcService;
import cn.skcks.docking.gb28181.wvp.config.ProxySipConfig;
import cn.skcks.docking.gb28181.wvp.config.WvpProxyConfig;
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.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 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.http.MediaType;
import org.springframework.stereotype.Service;

import javax.sdp.Connection;
import javax.sdp.SdpFactory;
import javax.sdp.TimeDescription;
import javax.sip.ListeningPoint;
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.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 ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class VideoInfo {
        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 header(HttpServletResponse response, String fileName) {
        header(response);
        response.setHeader("Content-Disposition",
                MessageFormat.format("attachment; filename=\"{0}.mp4\"",fileName));
    }

    private String videoUrl(String streamId) {
        return StringUtils.joinWith("/", zlmMediaConfig.getUrl(), "rtp", streamId + ".live.mp4");
    }

    @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
    public void video(HttpServletRequest request, HttpServletResponse response, String deviceCode, Date startTime, Date endTime, Boolean fileHeader) {
        AsyncContext asyncContext = request.startAsync();
        asyncContext.setTimeout(0);
        asyncContext.start(()->{
            HttpServletResponse asyncResponse = (HttpServletResponse)asyncContext.getResponse();
            try{
                download(deviceCode, startTime,endTime).whenComplete((videoInfo, e)->{
                    writeFileHeader(response,deviceCode,startTime,endTime,fileHeader);
                    if(e != null){
                        writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
                    } else if(videoInfo == null){
                        writeErrorToResponse(asyncResponse, JsonResponse.error("下载失败"));
                    } else if(wvpProxyConfig.getUseFfmpeg()){
                        videoService.ffmpegRecord(asyncResponse, videoInfo.getUrl(), DateUtil.between(startTime,endTime,DateUnit.SECOND) + 60,videoInfo.getDevice(),videoInfo.getCallId());
                    } else {
                        videoService.javaCVrecord(asyncResponse, videoInfo.getUrl(), DateUtil.between(startTime,endTime,DateUnit.SECOND) + 60);
                    }
                    asyncContext.complete();
                });
            } catch(Exception e) {
                writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
                asyncContext.complete();
            }
        });
    }

    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);
        }
    }

    @SneakyThrows
    public CompletableFuture<VideoInfo> download(String deviceCode, Date startTime, Date endTime) {
        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);
        }
    }

    @SneakyThrows
    public CompletableFuture<VideoInfo> download(String gbDeviceId, String channel, Date startTime, Date endTime){
        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 = MediaSdpHelper.getStreamId(gbDeviceId, channel, String.valueOf(start), String.valueOf(end), IdUtil.getSnowflakeNextIdStr());
        int streamMode = proxySipConfig.getTransport().equalsIgnoreCase(ListeningPoint.UDP) ? 0 : 1;
        String ip = zlmMediaConfig.getIp();
        int port = openRtpServer(streamId, streamMode);
        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);
        GB28181Description gb28181Description = MediaSdpHelper.build(MediaSdpHelper.Action.DOWNLOAD, gbDeviceId, channel, Connection.IP4, ip, port, ssrc, StreamMode.of(ListeningPoint.UDP), timeDescription);
        gb28181Description.setSessionName(SdpFactory.getInstance().createSessionName(MediaSdpHelper.Action.DOWNLOAD.getAction()));
        gb28181Description.setTimeDescriptions(new Vector<>(){{add(timeDescription);}});
        MediaDescriptionImpl media = (MediaDescriptionImpl) gb28181Description.getMediaDescriptions(true).get(0);
        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, MediaSdpHelper.Action.DOWNLOAD, ssrc, streamId, result, time));
        return result;
    }

    public SipSender.SendRequest inviteRequest(WvpProxyDocking docking, WvpProxyDevice device, GB28181Description description, MediaSdpHelper.Action action, String ssrc, String streamId, CompletableFuture<VideoInfo> result, long time) {
        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, ssrc, streamId, result, time + 60, TimeUnit.SECONDS);
            subscribe.getInviteSubscribe().addSubscribe(subscribeKey, subscriber);
            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 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();
                    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,streamId, time, unit));
                        return SipRequestBuilder.createAckRequest(Response.OK, ip, port, docking, device.getGbDeviceChannelId(), fromTag, toTag, callId);
                    }));
                    result.complete(new VideoInfo(videoUrl(streamId), callId, device));
                } 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);
            }
        };
        schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, time, unit);
        return subscriber;
    }

    public Flow.Subscriber<SIPRequest> byeSubscriber(String key,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) {
                String transport = request.getTopmostViaHeader().getTransport();
                String ip = request.getLocalAddress().getHostAddress();
                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);
                schedule[0].cancel(true);
                zlmMediaService.closeRtpServer(new CloseRtpServer(streamId));
            }
        };
        schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, time, unit);
        return subscriber;
    }
}
